在项目开发中,大部分 Android 开发者都使用过 ButterKnife 这个通过注解简化代码的框架。部分 Android 开发者也使用过 xUtils 这种快速开发的框架。
两者均使用到了注解。在上一篇 Java注解、反射,安卓IOC(一) 中我们知道了注解及反射的用法。本篇会分别介绍下其实现机制。
运行时注解
首先我们自己简单实现类似 xUtils 这种运行时注解框架。
绑定 View 控件
创建注解:
1 2 3 4 5 6 
  | @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface FindView {     int value() default -1; } 
  | 
 
View解析代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 
  | public class ViewInject {     public static void bind(Activity activity) {         inject(new ViewFinder(activity), activity);     }     public static void bind(View view) {         inject(new ViewFinder(view), view);     }     public static void bind(View view, Object obj) {         inject(new ViewFinder(view), obj);     }     private static void inject(ViewFinder finder, Object obj) {         injectFields(finder, obj);         injectMethods(finder, obj);     }     private static void injectFields(ViewFinder finder, Object obj) {         Class<?> clazz = obj.getClass();         Field[] fields = clazz.getDeclaredFields();         for (Field field : fields) {             if (field.isAnnotationPresent(FindView.class)) {                 FindView findView = field.getAnnotation(FindView.class);                 if (findView.value() < 0) {                     throw new IllegalArgumentException("The id can't be -1.");                 } else {                     View view = finder.findViewById(findView.value());                     try {                         field.setAccessible(true);                         field.set(obj, view);                      } catch (IllegalAccessException e) {                         e.printStackTrace();                     }                 }             }         }     } } 
  | 
 
主要流程为通过反射获取并遍历所有变量,若变量被注解修饰,则将注解的 ID 赋值给指定方法并调用。
绑定 OnClick 事件
创建注解:
1 2 3 4 5 6 
  | @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)  public @interface OnClick {     int[] id(); } 
  | 
 
OnClick 事件注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 
  | private static void injectMethods(ViewFinder finder, final Object obj) {     Method[] methods = obj.getClass().getDeclaredMethods();     for (final Method method : methods) {         if (method.isAnnotationPresent(OnClick.class)) {             OnClick onClick = method.getAnnotation(OnClick.class);             if (onClick.id().length != 0) {                 for (int i : onClick.id()) {                     View view = finder.findViewById(i);                     method.setAccessible(true);                     view.setOnClickListener(new View.OnClickListener() {                         @Override                         public void onClick(View v) {                             try {                                 method.invoke(obj, v);                             } catch (IllegalAccessException e) {                                 e.printStackTrace();                             } catch (InvocationTargetException e) {                                 e.printStackTrace();                             }                         }                     });                 }             }         }     } } 
  | 
 
主要流程为通过反射获取并遍历所有方法,若方法被注解修饰,遍历所有的 ID ,将注解的 ID 赋值给 findViewById 方法,然后在 setOnClickListener 调用 method 方法。
在 Activity 中的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 
  | public class IocActivity extends AppCompatActivity {     @FindView(R.id.txt_ioc_test)     private TextView mTxtTest;     @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_ioc);         ViewInject.bind(this);         mTxtTest.setText("测试");     }     @OnClick(id = {R.id.btn_ioc_test, R.id.btn_ioc_test2})     public void onClick(View view) {         switch (view.getId()) {             case R.id.btn_ioc_test:                 Toast.makeText(this, "Click1", Toast.LENGTH_SHORT).show();                 break;             case R.id.btn_ioc_test2:                 Toast.makeText(this, "Click2", Toast.LENGTH_SHORT).show();                 break;         }     } } 
  | 
 
以上为运行时注解的简单实现,即 xUtils 使用的注解方法。但是这种方法因为通过一系列反射获取属性、方法等,对性能会有所影响,所以不建议在实际项目中使用,下边介绍下轻量级的编译时注解。
编译时注解
ButterKnife 源码解析网上已经有很多不错的文章了,例如这篇 ButterKnife源码分析 讲的就很好。这里主要介绍下自己的大致实现以及编译时注解在 Android Studio 中的使用。
首先介绍下大概的项目结构,如下图所示:

- annotation module: Java library  - 定义一系列注解。
 
- injetc module: Android library  - 定义注解的接口及调用方法。
 
- compiler module: Java library  - 自定义编译时注解 AbstractProcessor 在编译期间生成 java 代码。
 
- app: 使用方法。
 
本篇文章主要为介绍及学习,所以此处仅实现 setContentView 的编译时注解。
1、声明注解
1 2 3 4 5 6 
  | @Retention(RetentionPolicy.CLASS)  @Target(ElementType.TYPE)  public @interface ContentView {     int value(); } 
  | 
 
2、声明外界接口及方法
声明接口:
1 2 3 4 
  | public interface ContentInjector<T> {     void injectContent(T obj, Activity activity);  } 
  | 
提供方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 
  | public class ContentViewInject {     public static void bind(Activity activity) {         injectContentView(activity);     }     private static void injectContentView(Activity activity) {         Class<? extends Activity> clazz = activity.getClass();         try {             ContentInjector injector = (ContentInjector) Class.forName(clazz.getName()                     + "$$ViewBinder").newInstance();             injector.injectContent(activity, activity);         } catch (InstantiationException e) {             e.printStackTrace();         } catch (IllegalAccessException e) {             e.printStackTrace();         } catch (ClassNotFoundException e) {             e.printStackTrace();         }     } } 
  | 
 
以上代码为简单使用,butterknife 中的 UnBinder 解绑,使用 Map 缓存等暂不考虑。
此处的 Class.forName(“”) 以及 class.newInstance() 会对性能略有影响,butterknife 在此进行了 map 缓存优化。
3、自定义 AbstractProcessor,此处需将 module 设置为 Java library 才可继承 AbstractProcessor。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 
  | @AutoService(Processor.class) @SupportedSourceVersion(value = SourceVersion.RELEASE_7) public class ContentViewInjectProcessor extends AbstractProcessor {          @Override     public Set<String> getSupportedAnnotationTypes() {         Set<String> supportTypes = new LinkedHashSet<>();         supportTypes.add(ContentView.class.getCanonicalName());         return supportTypes;     }     @Override     public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {         String packageName;         String className;                  for (Element element : roundEnvironment.getElementsAnnotatedWith(ContentView.class)) {             if (element.getKind() == ElementKind.CLASS) {                 TypeElement typeElement = (TypeElement) element;                 PackageElement packageEle = (PackageElement) element.getEnclosingElement();                 packageName = packageEle.getQualifiedName().toString();                                  className = typeElement.getSimpleName().toString();                 int layoutId = typeElement.getAnnotation(ContentView.class).value();                                                   StringBuilder builder = new StringBuilder();                 builder.append("package ").append(packageName).append(";\n");                 builder.append("import android.view.View;\n");                 builder.append("import android.app.Activity;\n");                 builder.append("import com.freedom.lauzy.inject.ContentInjector;\n");                 builder.append('\n');                 builder.append("public class ").append(className + "$$ViewBinder");                 builder.append("<T extends ").append(className).append(">");                 builder.append(" implements ContentInjector<T>");                 builder.append(" {\n");                 builder.append("@Override\n")                         .append("public void injectContent(final T source, Activity activity) {\n");                 builder.append("    ((Activity) source).setContentView(" + layoutId);                 builder.append(");\n");                 builder.append("}\n\n}\n");                                  try {                     JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(                             packageName + "." + className + "$$ViewBinder",                             typeElement);                     Writer writer = fileObject.openWriter();                     writer.write(builder.toString());                     writer.flush();                     writer.close();                 } catch (IOException e) {                     e.printStackTrace();                     System.out.println("error");                 }             }         }         return true;     } } 
  | 
 
butterknife 中使用了 javapoet 生成 Java 代码文件
此 module 的 gradle 配置如下:
1 2 3 4 5 6 7 8 9 10 11 
  | apply plugin: 'java' dependencies {     compile fileTree(include: ['*.jar'], dir: 'libs')     compile 'com.google.auto.service:auto-service:1.0-rc2'      compile project(':annotation') } 
 sourceCompatibility = "1.7" targetCompatibility = "1.7" 
  | 
4、app 使用
app 的 gradle 配置如下:
1 2 3 4 5 6 7 
  | dependencies {     ...     annotationProcessor project(':compiler')     compile project(':annotation')     compile project(':inject') } 
  | 
activity 中使用:
1 2 3 4 5 6 7 8 9 10 
  | @ContentView(R.layout.activity_ioc) public class IocActivity extends AppCompatActivity {     @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         ContentViewInject.bind(this);     } } 
  | 
 
这样,整个流程就结束了。此时 build 整个项目则会在 app/build/generated/source/apt/debug/com.lauzy.freedom.lauzycode/IOC 文件夹下生成一个
IocActivity$$ViewBinder 的 Java 文件,代码如下:
1 2 3 4 5 6 7 8 9 10 11 
  | package com.lauzy.freedom.lauzycode.IOC; import android.app.Activity; import com.freedom.lauzy.inject.ContentInjector; public class IocActivity$$ViewBinder<T extends IocActivity> implements ContentInjector<T> { @Override public void injectContent(final T source, Activity activity) {     ((Activity) source).setContentView(2130968606); } } 
  | 
 
可以看到,其实是在编译时生成了一个 Java 文件,并在 activity 的 onCreate 方法中调用了 setContentView 方法。
注意事项:
1、若不使用 com.google.auto.service:auto-service:1.0-rc2 这个 google 的生成源代码库,则需要手动创建一个 META_INF 文件来指定注解。
需要在 compiler 中创建 compiler/src/main/resources/META-INF/services 目录(注意 META-INF 中间不是下划线,减号即可),并新建 text 文件 javax.annotation.processing.Processor,
文件中写入自定义 AbstractProcessor 的全名称,如: com.lauzy.ContentViewInjectProcessor 。多个的话换行写入。
如下图所示:

2、本文使用 annotationProcessor 的注解处理器代替 android-apt ,Google 内置的注解处理器,建议使用。
分析
编译时注解的优点 :在于对性能影响很小的情况下,大量简化程序员的代码,像 butterknife 在首次查找类的时候对性能稍有影响,其他情况下影响微乎其微。
编译时注解的缺点 :build 过程生成更多的代码,增加了类和方法的数量;对性能影响很小,但是多少会有的。
本人认为编译时注解在优化代码,提高效率方面是有很大优势的,远远大于其缺点。
本篇文章主要是分析及梳理大致的实现方式,仅为学习使用,所有代码均托管在 我的Github 上。