注解
分类
按运行机制分:通过@Retention注解进行分类
- 源码注解:只在源码中存在。RetentionPolicy.SOURCE,编译为class字节码之后丢失
 
- 编译时注解:在class中依然存在,RetentionPolicy.CLASS。
 
- 运行时注解:运行阶段可见,RetentionPolicy.RUNTIME。
 
按来源分:
- JDK自带注解
- 内置注解:如@Override,@Deprecated、@SurpressWarnings等
 
- 元注解:用于修饰其他注解
- @Retention:注解作用域(可见性、保留策略),分为SOURCE、CLASS(默认策略)、RUNTIME
 
- @Target:注解作用对象,未指定时可以作用在任何元素上。可以传入数组,如
@Target({TYPE, Field, ...}) 
- @Document:可以使用javadoc工具生成文档
 
- @Inherited:父类使用了@Inherited修饰的注解,子类也会继承该注解。
- 接口使用了@Inherited修饰的注解,实现类不会继承该注解
 
- 父类方法使用了@Inherited修饰的注解,子类不会继承该注解
 
 
- @Repeatable:JDK1.8之后引入。表示一个注解可以在一个元素上使用多次
 
 
 
- 三方注解:三方库提供的注解。如Hibernate,Struts,ButterKnife,ARouter等
 
- 自定义注解
 
定义
同class、interface一样,注解也是一种类型。
使用@interface关键字定义,不支持继承其他注解或接口,会自动继承Annotation接口。示例如下
1 2 3 4 5 6 7
   | @Documented  @Retention(RetentionPolicy.RUNTIME)  @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})  public @interface MyAnnotation {      String[] value() default {}; }
   | 
 
用途
- IDE检测和提示编码错误或警告
 
- 编译时处理:通过APT检测注解,生成代码、文档等(注解作用域:SOURCE、CLASS)。会增加代码量
 
- 运行时处理:反射解析注解,反射修改变量、调用方法等(注解作用域:RUNTIME)。会影响性能
 
需要注意的是:注解本身没有作用,只是一个标签,需要编写代码来提取和处理注解信息,简称APT(Annotation Processing Tool,注解处理器)
实例
- JUnit
 
- Android Annotations:配合IDE和Lint工作
- 防止代码混淆:@Keep
 
- 检查资源类型:@ColorRes
 
- 参数是否可空:@Nullable、@NonNull
 
- 指定方法需要在特定线程执行:@UiThread、@MainThread、@WorkThread、@BinderThread
 
- 限定取值范围:@IntRange、@Size(数组或集合大小)
 
- 权限检查:@RequiresPermission
 
- 使用@IntDef或@StringDef替代枚举:见下例。
 
 
- ButterKnife:高版本AGP(Android Gradle Plugin)生成的R文件不再是常量。可以添加ButterKnife提供的插件,生成R2资源id解决。
 
- Dagger
 
- DataBinding
 
- EventBus3
 
- DBFlow
 
- Retrofit
 
- …
 
使用@IntDef或@StringDef替代枚举
一般我们使用枚举来限制取值,但是枚举最终会生成对象,比常量占用更多内存。
源码中如View的VISIBLE/INVISIBLE/GONE、FOCUSABLE/NOT_FOCUSABLE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   | public class MainActivity extends Activity {   	     public static final int SUNDAY = 0;     public static final int MONDAY = 1;     
    	     @IntDef({SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY})     @Retention(RetentionPolicy.SOURCE)     public @interface WeekDays {     } 		     @WeekDays     int currentDay = SUNDAY;
      public void setCurrentDay(@WeekDays int currentDay) {         this.currentDay = currentDay;     }
      @WeekDays     public int getCurrentDay() {         return currentDay;     } }
  | 
 
运行时注解实现findViewById功能
避免写大量的findViewById代码
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
   |  @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @interface ViewInject {     int value(); }
  class ViewInjectUtil {     static void injectViews(Activity activity) {         Class<? extends Activity> activityCls = activity.getClass();          Field[] fields = activityCls.getDeclaredFields();          for (Field field : fields) {                           ViewInject viewInject = field.getAnnotation(ViewInject.class);             if (viewInject == null) continue;             int viewId = viewInject.value();              if (viewId == -1) continue;             try {                 field.setAccessible(true);                                                   field.set(activity, activity.findViewById(viewId));             } catch (IllegalAccessException) {                 e.printStackTrace();             }         }     } }
  public class ViewInjectActivity extends AppCompatActivity {     @ViewInject(R.id.viewInjectBtn1)     private Button mBtn1;
      @ViewInject(R.id.viewInjectBtn2)     private Button mBtn2;
      @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_view_inject);                  ViewInjectUtil.injectViews(this);     } }
 
  | 
 
运行时注解看起来不难,为什么要使用编译时注解?
运行时注解需要反射,项目复杂度越高,反射次数越多,性能较差。
编译时注解反射次数固定。
APT介绍
APT是javac的一个工具,可以在编译时扫描和处理注解,并生成java文件,参与编译。
原理:编译前端过程中javac进行词法分析和语法分析之后会生成抽象语法树(AST),然后调用注解处理器(注解处理器相当于javac对外提供的插件),可以在这个阶段生成新的java文件。也可以直接修改抽象语法树
- 编译前端:指Java源文件编译成class文件。
 
- 编译后端:class文件转为本地机器码
 
详情见编程语言、编译器和Android虚拟机
APT特点:
- 需要自定义APT处理器
 
- 需要手动拼接代理的代码:可以使用JavaPoet,调用API生成java源代码。如果是Kotlin,可以使用KotlinPoet
 
- 需要使用者手动调用代理类执行,或者通过门面对象,反射找到代理类并执行。如
ButterKnife.bind(this) 
- 会生成大量代理类,导致类和方法数增多
 
- 无法直接在源文件中插入或修改代码,而是生成代理类。
 
其实也可以通过tools.jar库中的API(如JCTree、TreeMaker)直接操作抽象语法树,插入代码。
需要添加依赖包:compileOnly files(org.gradle.internal.jvm.Jvm.current().getToolsJar())
可以参考Java Pluginable Annotation processing
代表框架:DataBinding、Dagger2、ButterKnife、EventBus3、DBFlow
KAPT:Kotlin注解处理器。kotlin代码生成Java AST,然后交给javac APT处理注解。生成Kotlin代码无法再被Kotlin编译器编译。(不同版本KAPT原理有一些差异)
为什么不使用ASM等字节码框架?
修改字节码复杂度较高,难以保证稳定,也不易于调试。
APT生成的Java类更好理解
下面介绍APT涉及到的相关知识点,内容较多,可以先跳到后面看自定义APT步骤,再回过头来看细节
AbstractProcessor介绍
自定义注解处理器需要继承AbstractProcessor,有几个关键的方法,如下
init:用于初始化变量 
process:最核心的方法,用于执行注解解析逻辑、生成文件。 
getSupportedAnnotationTypes:返回需要被处理的注解的完整类型名,其他注解会被过滤掉 
getSupportedOptions:返回注解处理器可以接收的参数。使用注解处理器的时候可以传入对应的参数值。 
getSupportedSourceVersion:返回支持的JDK版本 
其中getSupportedAnnotationTypes除了可以用返回值的方式指定之外,还可以用注解声明。AbstractProcessor中重写了该方法,反射解析对应的注解@SupportedAnnotationTypes,拿到注解的值然后返回。getSupportedOptions和getSupportedSourceVersion同理。
Tips:简单的可以使用注解指定,传入完整的类型名。多个注解的时候还是通过代码返回列表,不容易出错,而且路径改变的时候可以跟着一起改。
注解处理器传入参数
- 例如配置
@SupportedOptions("content") 
- 使用注解处理器的时候,传入参数,gradle配置如下:
 
1 2 3 4 5 6 7 8 9 10
   | android {     defaultConfig {                  javaCompileOptions {             annotationProcessorOptions {                 arguments = [content: 'Hello World']             }         }     } }
  | 
 
- 在注解处理器代码中通过
processingEnvironment.getOptions().get("content");拿到参数使用 
常用类和API
参考注解处理器常用类说明
Element
位于javax.lang.model.element.Element
Java文件是有规范和格式的,源代码有对应的结构体,主要包含以下几种元素(类似于XML文件中的DOM树):

PackageElement:表示包。提供对有关包及其成员的信息的访问 
-  
TypeElement:表示类或接口程序元素。提供对有关类型及其成员的信息的访问。 
-  
ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序 
-  
VariableElement:表示字段、enum常量、方法或构造方法参数、局部变量或异常参数 
TypeParameterElement:表示泛型 
1 2 3 4 5 6 7 8 9
   | package com.afauria.sample.apt; 
  public class Main {    private String test = "";       void main() {    }    }
   | 
 
常用API:
| 函数名 | 
作用 | 
| accept | 
访问者模式,传入ElementVisitor。用于访问该元素下的所有元素。根据元素类型进行访问 | 
| getEnclosedElements | 
获取该元素的所有直接子元素 | 
| getEnclosingElement | 
获取该元素的父元素 | 
| getKind | 
返回ElementKind枚举,例如VariableElement可以表示字段、参数等,通过ElementKind可以判断元素具体类型 | 
| getSimpleName | 
返回元素名字,如变量名,类名 | 
| getQualifiedName | 
获取全名 | 
| getModifiers | 
获取修饰符集合Set<Modifier>,如public、native、synchronized等 | 
| getParameters | 
获取方法参数列表List<? extends VariableElement> | 
| getReturnType | 
获取方法返回值 | 
| asType | 
返回对应的TypeMirror | 
TypeMirror
位于javax.lang.model.type.TypeMirror
Element只能表示Java文件结构,不能获取具体的Java类型,需要使用TypeMirror相关的类

常用API:
| 函数名 | 
作用 | 
| accept | 
访问者模式,传入TypeVisitor。根据子类类型进行访问 | 
| getKind | 
返回具体类型。如boolean、String等 | 
辅助类
javax.lang.model.util.Elements:用于操作Element,可以通过processingEnvironment.getElementUtils();获取 
javax.lang.model.util.getType:用于操作TyoeMirror,可以通过processingEnvironment.getTypeUtils();获取 
javax.annotation.processing.Messager:用于输出信息,可以通过processingEnvironment.getMessager();获取,打印error并不会中断process执行,但是会导致编译失败,无法执行下一个Task。 
javax.annotation.processing.Filer:用于生成文件,可以通过processingEnvironment.getFiler();获取。新生成的文件分为三类,会被放到不同路径
createSourceFile:源代码文件 
createClassFile:类文件 
createResource:资源文件 
 
注册JavaSPI服务配置文件
APT是如何找到我们自定义的Processor注解并执行的呢?
使用了JavaSPI(Service Provider Interface,服务发现接口)机制。关于JavaSPI机制可以阅读另一篇文章
APT运行的时候加载Processor接口,通过ServiceLoader读取services下的服务配置文件,找到Processor接口的实现类(可以有多个),遍历初始化和执行多个注解处理器。(类似于AndroidManifest注册组件)
有两种注册方式,如下:
手动创建service文件
main目录下面新建resources/META_INF/services/javax.annotation.processing.Processor文件,在文件中写入完整的自定义注解处理器类名(配置多个注解处理器的时候需要换行)。如下
1
   | butterknife.compiler.ButterKnifeProcessor
   | 
 
使用AutoService库生成service文件
原理:通过APT解析@AutoSerivce自动生成文件。
- 添加依赖包和对应的注解处理器
 
1 2 3 4
   | dependencies {     compileOnly "com.google.auto.service:auto-service-annotations:1.0"      annotationProcessor "com.google.auto.service:auto-service:1.0"  }
  | 
 
- 使用
@AutoService(Processor.class),即可自动生成SPI文件,如下 
1 2 3 4 5
   | 
  @AutoService(Processor.class) public final class AptProcessor extends AbstractProcessor { }
 
  | 
 
【踩坑】:低版本gradle直接compileOnly依赖即可,会自动应用annotationProcessor,高版本gradle需要单独使用annotationProcessor配置注解处理器。
增量注解处理器
具体可以参考官方文档、Gradle编译时注解增量教程
- 全量编译:改动一个注解会删除之前已经生成过的文件,再重新生成新的文件,效率较低。
 
- 增量注解:从
Gradle 4.7开始,gradle提供了增量apt,可以使上层开发者更快的编译。 
增量注解处理器类型
- isolating注解处理器:最快的增量注解处理器。一个注解处理器只处理一个注解
 
- aggregationg注解处理器:可以处理多个注解。注解的Retention必须是
CLASS or RUNTIME 
- dynamic注解处理器:重写
AbstractProcessor.getSupportedOptions函数,在执行APT的时候根据自身情况决定使用哪一种增量注解处理器类型 
配置方式如下:
手动配置
新建resources/META-INF/gradle/incremental.annotation.processors文件,在文件中写入注解处理器全称和增量注解处理器类型。如下:
1
   | com.afauria.sample.apt_processor.AptProcessor,DYNAMIC
   | 
 
使用incap-processor自动生成
原理:通过APT解析@IncrementalAnnotationProcessor自动生成文件,类似AutoService库。
- 添加依赖包和对应的注解处理器
 
1 2 3 4
   | dependencies {     compileOnly "net.ltgt.gradle.incap:incap:0.2"      annotationProcessor "net.ltgt.gradle.incap:incap-processor:0.2"  }
  | 
 
- 使用
@IncrementalAnnotationProcessor注解配置增量类型,如下 
1 2 3
   | @IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.DYNAMIC) public class AptProcessor extends AbstractProcessor { }
   | 
 
应用注解处理器
定义好注解处理器之后,如何使用呢?
- Gradle2.2之前,需要先依赖三方插件
apply plugin: 'com.neenbedankt.android-apt',在dependencies中添加apt project(':xxx-processor') 
- Gradle2.2之后,Gradle内置了了APT工具,直接在dependencies中添加
annotationProcessor project(':xxx-processor') 
- kotlin中先依赖插件
apply plugin: 'kotlin-kapt',dependencies中添加kapt project(':xxx-processor') 
自定义APT步骤
上面介绍了APT涉及到的知识点,比较零散,简单串联一下整个过程:
- 创建一个Java Module,用于定义注解,如
xxx-annotation 
- 创建一个Android Module,用于封装API接口,供使用者调用,如
xxx-library,依赖xxx-annotation 
- 编写注解处理器代码
- 创建一个Java Module:如
xxx-processor、xxx-compiler,依赖xxx-annotation 
- 定义
XXProcessor类,继承自AbstractProcessor 
- 重写
getSupportedAnnotationTypes方法,返回要被检测的注解 
- 重写
process方法解析注解,生成对应的文件,有几种方式:
- 手动拼接源代码
 
- 定义模版java文件,通过关键字匹配替换
 
- 使用JavaPoet,调用JavaAPI生成,可以自动缩进、导入import包,不容易出错
 
 
 
- 注册注解处理器:JavaSPI机制
 
- 注册增量注解处理器类型
 
- 应用注解处理器:
annotationProcessor "xxx-processor" 
- 使用者依赖
xxx-library 
- 源代码中使用注解
 
- 编译时扫描注解,生成代理类
 
- 调用生成的代理类方法,或者通过
xxx-library中的门面对象,反射找到代理类并执行。 
流程图就不画了,可以参考ButterKnife解析
为什么需要分三个module?
xxx-processor只有编译时会用到,打包的时候不需要依赖,使用compileOnly即可,因此和xxx-library分开。
xxx-annotation会被其他两个模块使用,因此单独抽一个module处理
xxx-library:主要封装API供外部调用。也可以不定义该模块,使用自定义注解+注解处理器,生成代理类之后,直接调用代理类方法
自定义APT示例
代码上传至仓库
确认目标功能和生成的类信息
先确认下我们的目标功能,调用方代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
   |  public class AptActivity extends AppCompatActivity {     @AptBindRes(R.string.app_name)     String text1;     @AptBindView(R.id.aptBtn1)     Button btn1;     @AptOnClick     void onBtn1Click() {         Log.e("AptActivity", "onBtn1Click: " + text1);     }     @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_apt);                  new AptActivity_ViewBinding(this);                       } }
 
  | 
 
以AptActivity为例,需要生成的类如下:APT的作用就是帮我们自动生成XXX_ViewBinding类,免去手写的过程
1 2 3 4 5 6 7 8 9 10 11 12 13
   | public final class AptActivity_ViewBinding {     public void AptActivity_ViewBinding(final AptActivity target) {         target.text1 = target.getString(R.string.app_name);         target.btn1 = target.findViewById(R.id.aptBtn1);                  target.findViewById(R.id.aptBtn1).setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View v) {                 target.onBtn1Click();             }         });     } }
  | 
 
编写代码
- 首先创建
java-library,名为apt-annotation,用于自定义注解@AptBindRes、@AptBindView、@AptOnClick 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   |  @Retention(RetentionPolicy.SOURCE) @Target(ElementType.FIELD) public @interface AptBindString {     int value(); }
  @Retention(RetentionPolicy.SOURCE) @Target(ElementType.FIELD) public @interface AptBindView {     int value(); }
  @Retention(RetentionPolicy.SOURCE) @Target(ElementType.METHOD) public @interface AptOnClick {     int value(); }
 
  | 
 
- 创建
java-library,名为apt-processor,用于自定义注解处理器AptProcessor。配置依赖如下 
1 2 3 4 5 6 7 8
   | dependencies {     implementation project(":apt-annotation")	     compileOnly "com.squareup:javapoet:1.12.1"	     compileOnly "com.google.auto.service:auto-service-annotations:1.0"	     annotationProcessor "com.google.auto.service:auto-service:1.0"	     compileOnly "net.ltgt.gradle.incap:incap:0.2"	     annotationProcessor "net.ltgt.gradle.incap:incap-processor:0.2"	 }
  | 
 
- 自定义注解处理器:
 
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
   |  @AutoService(Processor.class)
  @IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.DYNAMIC) public class AptProcessor extends AbstractProcessor {     private Filer mFiler;     private Messager mMessager;     private Elements mElements;          @Override     public synchronized void init(ProcessingEnvironment processingEnvironment) {         super.init(processingEnvironment);         mFiler = processingEnvironment.getFiler();         mMessager = processingEnvironment.getMessager();         mElements = processingEnvironment.getElementUtils();     }          @Override     public Set<String> getSupportedAnnotationTypes() {         Set<String> types = new LinkedHashSet<>();         types.add(AptBindView.class.getCanonicalName());         types.add(AptBindString.class.getCanonicalName());         types.add(AptOnClick.class.getCanonicalName());         return types;     }   	     private void error(Element element, String msg) {         mMessager.printMessage(Diagnostic.Kind.ERROR, msg, element);     } }
 
  | 
 
- 定义一个
builderMap,用于保存需要生成的类的信息,并拼接Java源代码。每解析一个元素,放到对应的类信息中。 
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
   | 
 
  Map<TypeElement, FileBuilder> builderMap = new HashMap<>();
  private FileBuilder getOrCreateBuilder(TypeElement element) {     FileBuilder builder = builderMap.get(element);     if (builder == null) {         builder = new FileBuilder(element);         builderMap.put(element, builder);     }     return builder; } class FileBuilder {          TypeElement mTypeElement;     String mPackageName;     String mClassName;          List<BindResourceInfo> mBindingResources = new ArrayList<>();          List<BindViewInfo> mBindingViews = new ArrayList<>();          List<BindMethodInfo> mBindingMethods = new ArrayList<>();     public FileBuilder(TypeElement typeElement, Elements elementsUtil) {         mTypeElement = typeElement;         mPackageName = elementsUtil.getPackageOf(typeElement).toString();         mClassName = typeElement.getSimpleName() + "_ViewBinding";     }        	public String generateJavaCode() {         StringBuilder sb = new StringBuilder();                  sb.append("package ").append(mPackageName).append(";\n");         sb.append("\nimport android.view.View;\n");                  sb.append("\npublic class ").append(mClassName).append(" {\n");                  sb.append("\tpublic ").append(mClassName).append("(final ").append(mTypeElement.getSimpleName()).append(" target) {\n");                  for (BindResourceInfo bindResourceInfo : mBindingResources) {             sb.append(bindResourceInfo.bindResourceCode());         }                  for (BindViewInfo bindViewInfo : mBindingViews) {             sb.append(bindViewInfo.bindViewCode());         }                  for (BindMethodInfo bindMethodInfo : mBindingMethods) {             sb.append(bindMethodInfo.bindMethodCode());         }         sb.append("\t}\n");         sb.append("}\n");         return sb.toString();     } }
 
  class BindResourceInfo {     int resId;     String fieldName;          public String bindResourceCode() {         return String.format("\t\ttarget.%s = target.getString(%s);\n", fieldName, resId);     } } class BindViewInfo {     int resId;     String fieldName;          public String bindViewCode() {         return String.format("\t\ttarget.%s = target.findViewById(%s);\n", fieldName, resId);     } } class BindMethodInfo {     int resId;     String methodName;          public String bindMethodCode() {         StringBuilder sb = new StringBuilder();         sb.append("\t\ttarget.findViewById(").append(resId).append(").setOnClickListener(new View.OnClickListener() {\n");         sb.append("\t\t\tpublic void onClick(View view) {\n");         sb.append("\t\t\t\ttarget.").append(methodName).append(";\n");         sb.append("\t\t\t}\n");         sb.append("\t\t});\n");         return sb.toString();     } }
 
  | 
 
- 重写
process方法,解析和处理注解,将需要的信息填入Builder中 
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
   | @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {          builderMap.clear();     if (set.isEmpty()) {         return false;     }          findAndParseBindingResources(roundEnvironment);          findAndParseBindingView(roundEnvironment);          findAndParseBindingMethod(roundEnvironment);     for (FileBuilder fileBuilder : builderMap.values()) {         try {           	             generateJavaFile(fileBuilder);                                   } catch (IOException e) {                          error(fileBuilder.mTypeElement, String.format("Unable to write ViewBinding for type %s: %s", fileBuilder.mTypeElement, e.getMessage()));         }     }     return true; } private void generateJavaFile(FileBuilder fileBuilder) throws IOException {          JavaFileObject javaFileObject = mFiler.createSourceFile(fileBuilder.mPackageName + "." + fileBuilder.mClassName);          String code = fileBuilder.generateJavaCode();     Writer writer = javaFileObject.openWriter();          writer.write(code);     writer.flush();     writer.close(); }
   | 
 
- 贴一下解析
@AptBindString的代码,视图绑定和事件绑定类似 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | private void findAndParseBindingResources(RoundEnvironment env) {          Set<? extends Element> s = env.getElementsAnnotatedWith(AptBindString.class);     for (Element element : s) {                  int resId = element.getAnnotation(AptBindString.class).value();                  final String name = element.getSimpleName().toString();                  TypeElement parentElement = (TypeElement) element.getEnclosingElement();                  FileBuilder builder = getOrCreateBuilder(parentElement);                  builder.mBindingResources.add(new BindResourceInfo(resId, name));     } }
  | 
 
使用JavaPoet
第4步拼接Java源代码也可以通过调用JavaPoet API来生成,上面build.gradle已经添加依赖包。最终生成的结果就不贴了,和上面目标一致。
直接贴代码:
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
   | public JavaFile generateJavaFileByPoet() {          MethodSpec.Builder constructor = MethodSpec.constructorBuilder()             .addModifiers(Modifier.PUBLIC)             .addParameter(TypeName.get(mTypeElement.asType()), "target", Modifier.FINAL);          for (BindResourceInfo bindResourceInfo : mBindingResources) {         constructor.addCode(bindResourceInfo.bindResourceCode());     }          for (BindViewInfo bindViewInfo : mBindingViews) {         constructor.addCode(bindViewInfo.bindViewCode());     }          for (BindMethodInfo bindMethodInfo : mBindingMethods) {                  TypeSpec listener = TypeSpec.anonymousClassBuilder("")                 .addSuperinterface(ClassName.get("android.view", "View", "OnClickListener"))                 .addMethod(MethodSpec.methodBuilder("onClick")                         .addAnnotation(Override.class)                         .addModifiers(Modifier.PUBLIC)                         .returns(TypeName.VOID)                         .addParameter(ClassName.get("android.view", "View"), "view")                         .addStatement("target.$N()", bindMethodInfo.methodName)                         .build())                 .build();                  constructor.addStatement("target.findViewById($L).setOnClickListener($L)", bindMethodInfo.resId, listener);     }          TypeSpec typeSpec = TypeSpec             .classBuilder(mClassName)             .addModifiers(Modifier.PUBLIC)             .addMethod(constructor.build())             .build();          return JavaFile.builder(mPackageName, typeSpec).build(); }
 
 
  | 
 
注:
addStatement会添加缩进、换行、分号结尾。addCode直接添加代码,不会添加缩进、换行、分号结尾 
- JavaPoet提供了一些字符串format的占位符,就是代码中的
$L、$N等,可以查看CodeBlock类中的注释说明。列举几个常用的
$L:表示字面量,可以是字符串、基础数据类型、代码块等。 
$N:表示名称,如参数名、字段名、局部变量名、方法名等 
$T:表示类型,强转或者调用类静态方法、静态属性的时候会用到 
$[:开始一个语句,会自动添加缩进 
$]:结束一个语句 
 
Tips:字面量就是指这个量本身,如String name = "Hello World";中, "Hello World"就是字面量(含引号)。
可以简单理解为就是=号右边的部分
更多JavaPoet API可以参考官方文档
封装API接口
上面客户端是直接使用生成的代理类new AptActivity_ViewBinding(this);。这么写有几个不足:
- 需要在build完之后才有代理类,没build之前代码会报红
 
- 不同类写法不一致,不好封装基类
 
因此我们封装一个API接口类,让客户端能够更方便的调用。
- 首先新建一个
Android Module,如apt-library,并依赖自定义注解库apt-annotation 
- 直接贴代码,看注释即可
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | public class AptBinder {          public static void bind(Activity activity) {         Class<?> cls = activity.getClass();                  String clsName = activity.getClass().getName() + "_ViewBinding";         try {                          Class<?> clazz = Class.forName(clsName);                                       clazz.getConstructor(cls).newInstance(activity);         } catch (Exception e) {             e.printStackTrace();         }     } }
  | 
 
- 客户端依赖
apt-library,Activity中调用AptBinder.bind(this)完成绑定 
可优化
实现的是简化版的自定义注解处理器,有很多可优化的点:
- 考虑注解不在Activity中(如Fragment、View、Adapter或者普通类中),如何获取资源、绑定视图:需要往生成类中传入context或者view。
 
- 添加校验:注解对象或方法非private或static,注解对象父元素
TypeElement不能是接口或枚举类等 
- 生成释放对象的代码,
unbind方法 
@OnClick注解绑定多个View解析:注解参数需要使用id数组 
- 监听器回调方法有参数的时候,如何获取并生成?
 
- 父类和子类都有注解,如何兼容?
 
- 资源id非final类型,如何处理?
 
- 在内部类中使用注解,如何处理?
 
- 更多资源注解,更多事件注解,如何保存builder类信息,减少重复代码?
 
- 增量编译这里没有实际用到
 
- …
 
结语
代码均上传至仓库
参考资料: