背景
最近接触新项目,项目中引入了Android Annotation(AA)依赖注入开源框架,代码中大片的注解代码,对于没用过注解框架(或者说没有如此大面积的使用)的我来说确实看得很费力,于是花时间研究了一下Android中的注解,当然了,这篇文章的目的并非讲解AA的使用,而是主要讲如何自定义注解以及用自定义的注解完成最常见的View Inject。
定义自己的注解
很简单,new->Java Class->选择Annotation,即完成了一个最基本的注解的定义。
public @interface TestAnnotation { }
顺带提一下,任何一个注解其实都是隐式地实现了java.lang.annotaion.Annotaion接口的。对于一个完备的自定义注解,我们通常还需要两个东西,一是注解的修饰,二是注解的参数。
注解的修饰
我们可以用注解的注解(又叫元注解)来修饰注解,常见的有两个:Retention和Target。
- Retention接收一个RetentionPolicy枚举作为value,用以确定注解的保持策略,其源码如下:
public enum RetentionPolicy { /** * Annotation is only available in the source code. */ SOURCE, /** * Annotation is available in the source code and in the class file, but not * at runtime. This is the default policy. */ CLASS, /** * Annotation is available in the source code, the class file and is * available at runtime. */ RUNTIME }
对于这三种类型,我的理解是这样的:
- SOURCE作为最弱的一种策略,被标注为RetentionPolicy.SOURCE的注解,只是作为写码时的一种提醒,告诉我们某个特殊的含义,就像Android Studio中关键字、类名或者方法名的颜色区分,编译器并不会读取它。比如@Override注解,只是为了表明该方法是从父类中重写的,即便我们注释掉该注解,也不影响程序的编译和运行。
- CLASS是缺省的保持策略,被标注为RetentionPolicy.CLASS的注解会影响到编译过程,但不影响到运行时。比如@TargetApi,可以用他来屏蔽掉编译时的非法版本API的错误,但是,如果你并没有在该注解标注的代码段内处理好新老版本的API使用问题的话,程序还是会在运行时发生异常。
- RUNTIME作为权限最高的策略,其保持策略会持续到运行时,因此可以使用反射来在运行时检测注解的存在与否,接下来的Demo中使用到的就是这种方式。
- Target接收一个ElementType枚举类型的数组作为参数,用以表示该注解的适用对象。常见的有FIELD、TYPE、METHOD、PARAMETER,顾名思义,不再赘述。
注解的参数
注解可以定义为需要多个参数,只要在使用该注解时按类型将实参传入对应的形参即可,如果形参定义时有默认值则可不传该参数,否则必须传。如下所示:
public @interface TestAnnotation { String param1(); String param2() default "hello"; int param3(); }
使用时:
@TestAnnotation(param1 = "world", param3 = 1) void test() { }
当然,如果你的注解只需要一个参数,你可以把参数方法名定义成缺省的value()方法,这样在传参时就可以省略掉"value=<yourValue>"前面的"value="。
View Inject实现
前面提到,既然注解可以维持到进行时,那我们是不是也可以像一些注解框架那样实现自己的视图注解,以缓解频繁的findViewbyId呢?首先想到的是定义一个ViewInject注解,该注解用于在标记某View会被自动findViewbyId,当然啦,我们需要一个传入一个id,所以我们的ViewInject类看起来会像这样:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface ViewInject { int value(); }
使用时会像这样:
@ViewInject(R.id.tv_inject) private TextView mTextView;
现在我们的注解定义和使用的完成了,我们该要解决如何完成我们的findViewById的动作的问题。这里我们开始用到一点反射的知识,首先我们通过反射获取到当前类的Fields集合,然后遍历集合,找出被标注为ViewInject注解的字段并拿出它们注解的value,也就是view在xml布局中的id,有了id我们就能为该字段获取到它相应的view了。我们的代码会像这样:
Class cls = getClass(); Field[] fields = cls.getDeclaredFields(); for (Field field : fields) { if (!field.isAnnotationPresent(ViewInject.class)) continue; ViewInject viewInject = field.getAnnotation(ViewInject.class); if (viewInject.value() <= 0) continue; field.setAccessible(true); try { field.set(this, findViewById(viewInject.value())); } catch (IllegalAccessException e) { e.printStackTrace(); } }
为了增加重用性,减少重复代码,我们很容易想到抽出一个BaseActivity,把这段代码从子类的onCreate方法拿到父类BaseActivity的onCreate方法中,但是这样问题就来了,如果进到父类的onCreate中时,子类还没有setContentView怎么办?没有根布局咋findView啊,愁人。。咋办呢,我们依葫芦画瓢,为Activity再定义一个注解并传入layout id,这下只要我们在BaseActivity中先反射拿到layout id并setContentView,就再不用担心没有根布局了,于是我们的注入代码进一步变成这样:
Class cls = getClass(); // 整体布局注入 if (!cls.isAnnotationPresent(ActivityInject.class)) return; ActivityInject layoutInject = (ActivityInject) cls.getAnnotation(ActivityInject.class); if (layoutInject.value() <= 0) return; setContentView(layoutInject.value()); // View变量注入 Field[] fields = cls.getDeclaredFields(); for (Field field : fields) { if (!field.isAnnotationPresent(ViewInject.class)) continue; ViewInject viewInject = field.getAnnotation(ViewInject.class); if (viewInject.value() <= 0) continue; field.setAccessible(true); try { field.set(this, findViewById(viewInject.value())); } catch (IllegalAccessException e) { e.printStackTrace(); } }
到这里,我们的View Inject其实已经完成了,剩下的就属于进阶~如何进阶呢,前面我们用到的都是FIED和TYPE级的注解来简化findview代码,接下来我们要用ElementType.METHOD来简化OnCreate中后续操作的执行和简化setOnClickListener的操作。新增两个注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AfterCreate { }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface OnClick { int[] value(); }
对应的在BaseActivity中新增注入的解析:
Method[] methods = cls.getDeclaredMethods(); ArrayList<Method> afterMethods = new ArrayList<>(); for (final Method method : methods) { // 暂存AfterCreate方法 if (method.isAnnotationPresent(AfterCreate.class)) { afterMethods.add(method); } // 处理OnClick方法 if (method.isAnnotationPresent(OnClick.class)) { OnClick onClick = method.getAnnotation(OnClick.class); View.OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View v) { try { Class[] paramClasses = method.getParameterTypes(); // 这里只处理了无参数和只有一个view作参数的情况,其他情况不invoke if (paramClasses.length == 0) { method.invoke(BaseActivity.this, null); } else if (paramClasses.length == 1) { if (paramClasses[0].equals(View.class)) { method.invoke(BaseActivity.this, v); } } } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }; for (int id : onClick.value()) { if (id > 0) findViewById(id).setOnClickListener(onClickListener); } } } // 执行AfterCreate方法 for (Method method : afterMethods) { try { method.invoke(this, null); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
在子Activity中使用:
@AfterCreate void init() { mTextView.setText("Set TextView Content"); mImageView.setImageResource(R.mipmap.ic_launcher); } @OnClick({R.id.iv_inject,R.id.tv_inject}) void onClick(View view) { switch (view.getId()) { case R.id.iv_inject: Toast.makeText(getApplicationContext(), "ImageView is clicked!", Toast.LENGTH_SHORT).show(); break; case R.id.tv_inject: Toast.makeText(getApplicationContext(), "TextView is clicked!", Toast.LENGTH_SHORT).show(); break; } }
到这里就基本结束了。完整的Demo从这里下载。