Android中的自定义注解(反射实现-运行时注解)
预备知识: Java注解基础 Java反射原理 Java动态代理 一、布局文件的注解 我们在Android开发的时候,总是会写到setContentView方法,为了避免每次都写重复的代码,我们需要使用注解来代替我们做这个事情,只需要在类Activity上声明一个ContentView注解和对应的布局文件就可以了。 @ContentView(R.layout.activity_main) public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewUtils.injectContentView(this); } } 从上面可以看到,上面代码在MainActivity上面使用到了ContentView注解,下面我们来看看ContentView注解。 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ContentView { int value(); } 这个注解很简单,它有一个int的value,用来存放布局文件的id,另外它注解的对象为一个类型,需要说明的是,注解的生命周期会一直到运行时,这个很重要,因为程序是在运行时进行反射的,我们来看看ViewUtils.injectContentView(this)方法,它进行的就是注解的处理,就是进行反射调用setContentView()方法。 public static void injectContentView(Activity activity) { Class a = activity.getClass(); if (a.isAnnotationPresent(ContentView.class)) { // 得到activity这个类的ContentView注解 ContentView contentView = (ContentView) a.getAnnotation(ContentView.class); // 得到注解的值 int layoutId = contentView.value(); // 使用反射调用setContentView try { Method method = a.getMethod("setContentView", int.class); method.setAccessible(true); method.invoke(activity, layoutId); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } 如果对Java注解比较熟悉的话,上面代码应该很容易看懂。 二、字段的注解 除了setContentView之外,还有一个也是我们在开发中必须写的代码,就是findViewById,同样,它也属于简单但没有价值的编码,我们也应该使用注解来代替我们做这个事情,就是对字段进行注解。 @ContentView(R.layout.activity_main) public class MainActivity extends AppCompatActivity { @ViewInject(R.id.btn1) private Button mButton1; @ViewInject(R.id.btn2) private Button mButton2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewUtils.injectContentView(this); ViewUtils.injectViews(this); } } 上面我们看到,使用ViewInject对两个Button进行了注解,这样我们就是不用写findViewById方法,看上去很神奇,但其实原理很简单。我们先来看看ViewInject注解。 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ViewInject { int value(); } 这个注解也很简单,就不说了,重点就是注解的处理了。 public static void injectViews(Activity activity) { Class a = activity.getClass(); // 得到activity所有字段 Field[] fields = a.getDeclaredFields(); // 得到被ViewInject注解的字段 for (Field field : fields) { if (field.isAnnotationPresent(ViewInject.class)) { // 得到字段的ViewInject注解 ViewInject viewInject = field.getAnnotation(ViewInject.class); // 得到注解的值 int viewId = viewInject.value(); // 使用反射调用findViewById,并为字段设置值 try { Method method = a.getMethod("findViewById", int.class); method.setAccessible(true); Object resView = method.invoke(activity, viewId); field.setAccessible(true); field.set(activity, resView); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } 上面的注释很清楚,使用的也是反射调用findViewById函数。 三、事件的注解 在Android开发中,我们也经常遇到setOnClickListener这样的事件方法。同样我们可以使用注解来减少我们的代码量。 @ContentView(R.layout.activity_main) public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewUtils.injectContentView(this); ViewUtils.injectEvents(this); } @OnClick({R.id.btn1, R.id.btn2}) public void clickBtnInvoked(View view) { switch (view.getId()) { case R.id.btn1: Toast.makeText(this, "Button1 OnClick", Toast.LENGTH_SHORT).show(); break; case R.id.btn2: Toast.makeText(this, "Button2 OnClick", Toast.LENGTH_SHORT).show(); break; } } } 布局文件如下: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:background="#70DBDB" android:orientation="vertical" tools:context="statusbartest.hpp.cn.statusbartest.MainActivity"> <Button android:id="@+id/btn1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Test1"/> <Button android:id="@+id/btn2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Test2"/> </LinearLayout> 可以看到,上面我们没有对Button调用setOnClickListener,但是当我们点击按钮的时候,就会回调clickBtnInvoked方法,这里我们使用的就是注解来处理的。下面先来看看OnClick注解。 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @EventBase(listenerType = View.OnClickListener.class, listenerSetter = "setOnClickListener", methodName = "onClick") public @interface OnClick { int[] value(); } 可以看到这个注解使用了一个自定义的注解。 @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface EventBase { Class listenerType(); String listenerSetter(); String methodName(); } 下面来看看注解的处理。 public static void injectEvents(Activity activity) { Class a = activity.getClass(); // 得到Activity的所有方法 Method[] methods = a.getDeclaredMethods(); for (Method method : methods) { // 得到被OnClick注解的方法 if (method.isAnnotationPresent(OnClick.class)) { // 得到该方法的OnClick注解 OnClick onClick = method.getAnnotation(OnClick.class); // 得到OnClick注解的值 int[] viewIds = onClick.value(); // 得到OnClick注解上的EventBase注解 EventBase eventBase = onClick.annotationType().getAnnotation(EventBase.class); // 得到EventBase注解的值 String listenerSetter = eventBase.listenerSetter(); Class<?> listenerType = eventBase.listenerType(); String methodName = eventBase.methodName(); // 使用动态代理 DynamicHandler handler = new DynamicHandler(activity); Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class<?>[] { listenerType }, handler); handler.addMethod(methodName, method); // 为每个view设置点击事件 for (int viewId : viewIds) { try { Method findViewByIdMethod = a.getMethod("findViewById", int.class); findViewByIdMethod.setAccessible(true); View view = (View) findViewByIdMethod.invoke(activity, viewId); Method setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType); setEventListenerMethod.setAccessible(true); setEventListenerMethod.invoke(view, listener); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } } 这个代码相对上面的要复杂一些,它使用到了动态代理,关于动态代理的基本用法可以看看前面我提到的预备知识。 public class DynamicHandler implements InvocationHandler { private final HashMap<String, Method> methodMap = new HashMap<String, Method>( 1); // 因为传进来的为activity,使用弱引用主要是为了防止内存泄漏 private WeakReference<Object> handlerRef; public DynamicHandler(Object object) { this.handlerRef = new WeakReference<Object>(object); } public void addMethod(String name, Method method) { methodMap.put(name, method); } // 当回到OnClickListener的OnClick方法的时候,它会调用这里的invoke方法 @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { // 得到activity实例 Object handler = handlerRef.get(); if (handler != null) { // method对应的就是回调方法OnClick,得到方法名 String methodName = method.getName(); // 得到activtiy里面的clickBtnInvoked方法 method = methodMap.get(methodName); if (method != null) { // 回调clickBtnInvoked方法 return method.invoke(handler, objects); } } return null; } }