仿照 ButterKnife 的 Android 注解实例

非淡泊无以明志,非宁静无以致远。这篇文章主要讲述仿照 ButterKnife 的 Android 注解实例相关的知识,希望能为你提供帮助。
什么是注解java.lang.annotation,接口 Annotation,在JDK5.0及以后版本引入。
注解处理器是 javac 的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。一个注解的注解处理器,以 Java 代码(或者编译过的字节码)作为输入,生成文件(通常是 .java 文件)作为输出。这些生成的 Java 代码是在生成的 .java 文件中,所以你不能修改已经存在的 Java 类,例如向已有的类中添加方法。这些生成的 Java 文件,会同其他普通的手动编写的 Java 源代码一样被 javac 编译。
基本的注解

  • @ Override--限定重写父类方法
  • @ Deprecated--标示已过时
  • @ SuppressWarning--抑制编译器警告
  • @ SafeVarargs--这货与Java7里面的堆污染有关,具体想了解的,到传送这里
JDK的元注解JDK除了提供上述的几种基本的注释外,还提供了几种注释,用于修饰其他的注解定义
  1. @retention  这个是决定你注释存活的时间的,它包含一个RetationPolicy的值成员变量,用于指定它所修饰的注释保留时间,一般有:
      • Retationpolicy.CLASS:编译器将把注释记录在类文件中,不过当Java的程序执行的时候,JVM将抛弃它。
      • Retationpolicy.SOURCE:Annotation只保留在原代码中,当编译器编译的时候就会抛弃它。
      • Retationpolicy.RUNTIME:在Retationpolicy.CLASS的基础上,JVM执行的时候也不会抛弃它,所以我们一般在程序中可以通过反射来获得这个注解,然后进行处理。
  2. @target  这个注解一般用来指定被修饰的注释修饰哪些元素,这个注解也包含一个值变量:
      • ElementType.ANNOTATION_TYPE:指定该注释只能修饰注释。
      • ElementType.CONSTRUCTOR:指定只能修饰构造器。
      • ElementType.FIELD:指定只能成员变量。
      • ElementType.LOCAL_VARIABLE:指定只能修饰局部变量。
      • ElementType.METHOD:指定只能修饰方法。
      • ElementType.PACKAGE:指定只能修饰包定义。
      • ElementType.PARAMETER:指定只能修饰参数。
      • ElementType.TYPE:指定可以修饰类,接口,枚举定义。
  3. 【仿照 ButterKnife 的 Android 注解实例】@Document 这个注解修饰的注释类可以被 javadoc 的工具提取成文档
  4. @Inherited 被他修饰的注解具有继承性

自定义注释上面讲了一些JDK自带的注释,那么我们现在就可以用这些JDK自带的注释来实现一些我们想要的功能。先一步一步地模仿 butterknife 的实现吧。
定义一个注解:
@Target(ElementType.FIELD) // 用于成员变量 @Retention(RetentionPolicy.CLASS) //注解保留在 class 文件, 当Java的程序执行的时候,JVM将抛弃它 public @interface BindView { int value(); }

注解定义的方式就是  @interface  和接口的定义方式就少一个 @ 哦,不要搞混了。里面有一个变量值时,就是我们使用的时候 @BindView  (R.id.textView)  指定的  R.id.textView  id,旨在自动注入 view 的 id。注解处理器先来说下注解处理器  AbstractProcessor。它是 javac 的一个工具,用来在编译时扫描和处理注解  Annotation,你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。
一个注解的注解处理器,以 Java 代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这些由注解器生成的.java代码和普通的.java一样,可以被javac编译。
因为  AbstractProcessor  是 javac 中的一个工具,所以在 android 的工程下没法直接调用。
新建一个 java library
在 build.gradle 引入相关 jar :
apply plugin: ‘java-library‘dependencies { implementation fileTree(dir: ‘libs‘, include: [‘*.jar‘]) api ‘com.squareup:javapoet:1.7.0‘ api ‘com.google.auto.service:auto-service:1.0-rc2‘ api project(‘:lib-annotation‘) }sourceCompatibility = "1.7" targetCompatibility = "1.7"//指定编译的编码 tasks.withType(JavaCompile){ options.encoding = "UTF-8" }

javapoet 和  auto-service 后面会讲到,这两个在注解处理器中有着极大的作用。
引入之后,就开始编写注解处理器了。
@AutoService(Processor.class) public class CustomProcessor extends AbstractProcessor { private static final String TAG = "CustomProcessor"; // 文件相关的辅助类 private Filer mFiler; // 元素相关的辅助类 private Elements mElements; // 元素相关的辅助类 private Elements mElementUtils; /** * 解析的目标注解集合 */ private Map< String, AnnotatedClass> mAnnotatedClassMap = new HashMap< > (); @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); mElementUtils = processingEnvironment.getElementUtils(); mFiler = processingEnvironment.getFiler(); }//核心处理逻辑,相当于java中的主函数main(),你需要在这里编写你自己定义的注解的处理逻辑 //返回值 true时表示当前处理,不允许后续的注解器处理 @Override public boolean process(Set< ? extends TypeElement> set, RoundEnvironment roundEnvironment) { mAnnotatedClassMap.clear(); try { processBindView(roundEnvironment); } catch (IllegalArgumentException e) { return true; }try { for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) { annotatedClass.generateFinder().writeTo(mFiler); } } catch (Exception e) { e.printStackTrace(); } return true; }@Override public Set< String> getSupportedAnnotationTypes() { Set< String> types = new LinkedHashSet< > ();
// 标明该注解处理器是为了处理 BindView 注解的 types.add(BindView.class.getCanonicalName()); return types; }private void processBindView(RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) { // element = tv1; AnnotatedClass annotatedClass = getAnnotatedClass(element); BindViewField field = new BindViewField(element); annotatedClass.addField(field);
// 通过上面方法调用,可以获取到注解元素,以及和注解元素相关的类名,通过注解元素获得被注解的成员变量名,后续会对其进行初始化 } }private AnnotatedClass getAnnotatedClass(Element element) {
// 通过注解元素获取其封装类,获得类的引用 TypeElement encloseElement = (TypeElement) element.getEnclosingElement(); // encloseElement.getSimpleName() = MainActivity; String fullClassName = encloseElement.getQualifiedName().toString(); // com.sjq.recycletest.MainActivity AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName); if (annotatedClass == null) {
// 存到map当中,不用每次都生成一次,这样一个类里面有多个注解的时候,可以加快处理速度 annotatedClass = new AnnotatedClass(encloseElement, mElementUtils); mAnnotatedClassMap.put(fullClassName, annotatedClass); } return annotatedClass; }@Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } }

  • init(ProcessingEnvironment processingEnvironment):每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入 ProcessingEnviroment 参数.ProcessingEnviroment 提供很多有用的工具类 Types 和 Filer。后面我们将看到详细的内容。
  • process(Set< ? extends TypeElement> set, RoundEnvironment roundEnvironment):这相当于每个处理器的主函数main()。你在这里写你的扫描,评估和处理注解的代码,以及生成Java文件。输入参数 RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。
  • getSupportedAnnotationTypes():这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换??句话说,你在这里定义你的注解处理器注册到哪些注解上。
  • getSupportedSourceVersion():用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 7 的话,你也可以返回 SourceVersion.RELEASE_7。推荐你使用前者。
在类前面,使用了  AutoService 注解,这个在 build.gradle 中已经引入。AutoService 注解处理器是 Google 开发的,用来生成  META-INF/services/ javax.annotation.processing.Processor  文件的,你只需要在你定义的注解处理器上添加  @AutoService(Processor.class)  就可以了,简直不能再方便了。 注解器处理流程首先我们先简单的说明一下  porcess  的处理流程:
  1. 遍历 env,得到我们需要的元素列表
  2. 将元素列表封装成对象,方便之后的处理,比如获取元素的各种属性等
  3. 通过 JavaPoet 库将对象以我们期望的形式生成 java 文件,来处理注解
第一步:遍历env,得到我们需要的元素列

if (element.getKind() == ElementKind.FEILD) { // 显示转换元素类型 TypeElement typeElement = (TypeElement) element; // 输出元素名称 System.out.println(typeElement.getSimpleName()); // 输出注解属性值 System.out.println(typeElement.getAnnotation(BindView.class).value()); } }

上面的代码和  processBindView  的代码是一样的。判断元素类型,在进一步处理。有些注解可能对类和方法是同时生效的,这时候,判断类型分别处理就显得非常有必要了。
getElementsAnnotatedWith  能够获取到添加该注解的所有元素列表

第二步:将元素列表封装成对象,方便之后的处理
其实不进行封装也是可以的,但是这样当我们在使用的时候,可能就需要在不同的地方写很多重复的代码,为此,可以进一步封装,当我们需要获取元素属性的时候,直接调用相关方法即可。
新建类  BindViewField.class  用来保存自定义注解  BindView  相关的属性,后续需要元素上的信息都可以从该类获取。
public class BindViewField { private VariableElement mFieldElement; private int mResId; public BindViewField(Element element) throws IllegalArgumentException { if (element.getKind() != ElementKind.FIELD) { throw new IllegalArgumentException(String.format("Only field can be annotated with @%s", BindView.class.getSimpleName())); }
// 该注解用于成员变量的,因此需要进行转化 mFieldElement = (VariableElement) element;
// 在进一步转化为注解类型 BindView bindView = mFieldElement.getAnnotation(BindView.class); mResId = bindView.value(); if (mResId < 0) { throw new IllegalArgumentException(String.format("value() in %s for field % is not valid", BindView.class.getSimpleName(), mFieldElement.getSimpleName())); } }public Name getFieldName() { return mFieldElement.getSimpleName(); }public int getResId() { return mResId; }public TypeMirror getFieldType() { return mFieldElement.asType(); } }

上述的  BindViewField  只能表示一个自定义注解  bindView  对象。很多时候,会同时存在很多其他注解,每一种注解都需要一个单独对象来管理属性。而一个类中很可能会有多个自定义注解,因此,对于在同一个类里面的注解,我们可以创建一个对象来进行管理,这就是 Annotation.class。
public class AnnotatedClass {//类 public TypeElement mClassElement; //类内的注解变量 public List< BindViewField> mFiled; //元素帮助类 public Elements mElementUtils; public AnnotatedClass(TypeElement classElement, Elements elementUtils) { this.mClassElement = classElement; this.mElementUtils = elementUtils; this.mFiled = new ArrayList< > (); }//添加注解变量 public void addField(BindViewField field) { mFiled.add(field); }//获取包名 public String getPackageName(TypeElement type) { return mElementUtils.getPackageOf(type).getQualifiedName().toString(); }//获取类名 private static String getClassName(TypeElement type, String packageName) { int packageLen = packageName.length() + 1; // type.getQualifiedName().toString() = com.sjq.recycletest.MainActivity return type.getQualifiedName().toString().substring(packageLen).replace(‘.‘, ‘$‘); } }

第三步: 通过 JavaPoet 库将对象以我们期望的形式生成 java 文件
通过上述两步成功获取了自定义注解的元素对象,但是还是缺少一步关键的步骤,缺少一步  findViewById(),实际上 ButterKnife 这个很出名的库也并没有省略  findViewById()这一个步骤,只是在编译的时候,在 build/generated/source/apt/debug 下生成了一个文件,帮忙执行了findViewById()这一行为而已。
同样的,我们这里也需要生成一个 java 文件,采用的是 JavaPoet 这个库。具体的使用 参考链接
public JavaFile generateFinder() {//构建 inject 方法 MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL) .addParameter(TypeName.OBJECT, "source") .addParameter(Utils.FINDER, "finder"); //inject函数内的核心逻辑, // host.btn1=(Button)finder.findView(source,2131427450); ----生成代码 // host.$N=($T)finder.findView(source,$L)----原始代码 // 对比就会发现这里执行了实际的findViewById绑定事件 for (BindViewField field : mFiled) { methodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)", field.getFieldName() , ClassName.get(field.getFieldType()), field.getResId()); }String packageName = getPackageName(mClassElement); // com.sjq.recycletest String className = getClassName(mClassElement, packageName); ClassName bindClassName = ClassName.get(packageName, className); // bindClassName.toString()com.sjq.recycletest.MainActivity//构建类对象,注意此处的 $$Injector,生成的类名是由我们自己来控制的 TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector") .addModifiers(Modifier.PUBLIC) .addSuperinterface(ParameterizedTypeName.get(Utils.INJECTOR, TypeName.get(mClassElement.asType())))//继承接口 .addMethod(methodBuilder.build()) // 添加方法 .build(); return JavaFile.builder(packageName, finderClass).build(); }

上述代码先生成一个方法名,再添加函数体,接着把这个方法添加到一个类当中,这个类名是按照一定的规则拼接的,这也是后面采用反射获取生成类名的关键所在。
到这里,大部分逻辑都已实现,用来绑定控件的辅助类也已通关 JavaPoet 生成了,只差最后一步,宿主注册,如同 ButterKnife 一般,ButterKnife.bind(this)
编写调用接口在 annotation-api 下新建 android library。
注入接口  Injector
  最终会调用该方法来实现注解。
public interface Injector< T> { void inject(T host, Object source, Finder finder); }

宿主通用接口Finder(方便之后扩展到view和fragment)
public interface Finder {Context getContext(Object source); View findView(Object source, int id); }

activity实现类 ActivityFinder
  顾名思义,就是通过 activity 的  findViewById 来找到某个 view。
public class ActivityFinder implements Finder{@Override public Context getContext(Object source) { return (Activity) source; }@Override public View findView(Object source, int id) { return ((Activity) (source)).findViewById(id); } }

核心实现类 ButterKnife
public class ButterKnife { // private static final ActivityFinder finder = new ActivityFinder();
// 用于存储已经绑定的class,避免重复绑定 private static Map< String, Injector> FINDER_MAP = new HashMap< > (); public static void bind(Activity activity) { bind(activity, activity); }private static void bind(Object host, Object source) { bind(host, source, finder); }private static void bind(Object host, Object source, Finder finder) { String className = host.getClass().getName(); try { Injector injector = FINDER_MAP.get(className); if (injector == null) {
// 此处拿到的类名就是通过注解生成的中间处理类,即 MainActivity$$Injector Class< ?> finderClass = Class.forName(className + "$$Injector");
// 通过反射拿到class实例 injector = (Injector) finderClass.newInstance(); FINDER_MAP.put(className, injector); } injector.inject(host, source, finder); } catch (Exception e) { e.printStackTrace(); } } }

  在 bind 方法内部,通过一定的规则拼接最后生成的 .java 文件类名,然后通过反射的方法拿到实例,最后,调用 injector.inject 的方法来完成初始化,该方法就是得通过注解来生成的。
主工程下调用  对应的按钮可以直接使用,不需要findViewById(),这样我们可以少写很多同样代码,逻辑上也变得非常清楚。
public class MainActivity extends AppCompatActivity {@BindView(R.id.annotation_tv) public TextView tv1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
// tv1 的初始化过程就是在bind过程中完成的 ButterKnife.bind(this); tv1.setText("annotation_demo"); } }

  选择 build 下的 make project,会进行编译:
最终生成的类名如下:
public class MainActivity$$Injector implements Injector< MainActivity> { @Override public void inject(final MainActivity host, Object source, Finder finder) {
// 最终还是通过 findViewById 来对 tv1 进行初始化 host.tv1=(TextView)finder.findView(source,2131165300); } }

  到此,该实例讲解结束。
总结:下面会对上面的实例的思想进行总结,方便大家进一步理解其实现原理:
1、先看代码层面的。可以看到在主工程先对成员变量 tv1 添加了注解,获得了 view 的 id;其次先调用 bind 方法,获取到 activity 实例。接着就开始调用 tv1.setText。可是有没有发现,tv1 没有初始化。其初始化过程就是在 bind 这个过程当中。2、注解生成代码过程。通过获取成员变量所属的类,根据类名和命名规则获取最后注解会生成的类名,通过反射的形式,调用其中的 inject 方法。inject 方法中会有 activity 的 this 引用,通过  findViewById 方法,即可为 tv1 初始化。这样后面调用  tv1.setText 就不会出现空指针了。
项目源码:https://download.csdn.net/download/szengjiaqi/10629127
不能下载的,留下邮箱,发你
参考文献:
1、Android的编译时注解APT实战(AbstractProcessor)
2、浅谈Android下的注解

 
p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px "PingFang SC" } span.s1 { font: 12.0px Helvetica }















    推荐阅读