android 编译时注解

前言

我们经常使用的一些第三方框架,比如:butterknife,通过一行注解就可以实现View 的“自动赋值”。

那么,这其中的原理是什么呢?

为了带大家更好的深入了解,本文将打造一个简单的 Demo,来说明这其中的原理。

Demo 虽然简单,但是完全按照 butterknife 实现的方式和原理打造。

实现思路

我们先看 Demo 的效果:

public class MainActivity extends AppCompatActivity { // 被注解的 View @BindView(R.id.tv) TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 为 tv 赋值 InjectHelper.inject(this); tv.setText("I am injected"); } } 

代码非常简单,分为两步:

  1. 通过 @BindView(R.id.tv) 指定被注解 View 的 Id 值;
  2. InjectHelper.inject(this);,具体为 tv 赋值的方法。

@BindView 注解没什么好解释的,我们重点看下 InjectHelper.inject(this); 的实现:

public class InjectHelper { public static void inject(Activity host) { // 1、 String classFullName = host.getClass().getName() + "$$ViewInjector"; try { // 2、 Class proxy = Class.forName(classFullName); // 3、 Constructor constructor = proxy.getConstructor(host.getClass()) // 4、 constructor.newInstance(host); } catch (Exception e) { e.printStackTrace(); } } } 
  1. 获得 View 所在 Activity 的类路径,然后拼接一个字符串“$$ViewInjector”。这个是编译时动态生成的 Class 的完整路径,也就是我们需要实现的,同时也是最关键的部分;
  2. 根据 Class 路径,使用 Class.forName(classFullName) 生成 Class 对象;
  3. 得到 Class 的构造函数 constructor 对象;
  4. 使用 constructor.newInstance(host) new 出一个对象,这会执行对象的构造方法,方法内部是我们为 MainActivity 的 tv 赋值的地方。

我们先看一个生成好的 “XXX$$ViewInjector” 示例:

public class MainActivity$$ViewInjector { public MainActivity$$ViewInjector(MainActivity activity) { activity.tv = (TextView)activity.findViewById(2131427422); } } 

到这里,我们大概知道了最关键的地方,就是如何生成 “XXX$$ViewInjector” 这个类了。

APT 实现方案

APT 是一种处理注解的工具,它对源代码文件进行检测找出其中的 Annotation,再根据注解自动生成代码。

实现方案,分为两种:

  • android-apt,个人开发者提供,现在已经停止维护,作者推荐大家使用官方提供的解决方案。
  • Android Gradle 插件:annotationProcessor,由官方提供支持。

如何由 android-apt 切换到 annotationProcessor,可以参考这里

annotationProcessor 配置起来比较简单,另外由于是官方支持的,所以我们选择第二种方案。

实现步骤

第一步:定义注解 @BindView

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView { int value(); } 

没什么好解释的~

第二步:实现 AbstractProcessor

1、新建一个 Java Library,引入两个第三方库:

dependencies {
    // ...
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.7.0'
}
  • auto-service:Google 公司出品,用于自动为 JAVA Processor 生成 META-INF 信息。

    如果你定义了一个 Processor:

      package foo.bar;
      
      import javax.annotation.processing.Processor;
      
      @AutoService(Processor.class)
      final class MyProcessor implements Processor { // … } 

    auto-service 会在编译目录生成一个文件,路径是:META-INF/services/javax.annotation.processing.Processor,文件内容为:

      foo.bar.MyProcessor
    
  • javapoet:大名鼎鼎的 squareup 公司出品,封装了一套生成 .java 源文件的 API。

    以 HelloWorld 类为例:

      package com.example.helloworld;
    
      public final class HelloWorld { public static void main(String[] args) { System.out.println("Hello, JavaPoet!"); } } 

    上面的代码就是使用javapoet用下面的代码进行生成的:

      MethodSpec main = MethodSpec.methodBuilder("main")
          .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
          .returns(void.class)
          .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) .build(); javaFile.writeTo(System.out); 

    更多关于 javapoet 的使用,可以参考这里

2、继承 AbstractProcessor,配置相关信息:

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.example.BindView"})
@SupportedSourceVersion(SourceVersion.RELEASE_7) public class ViewInjectProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //... } } 
  • @AutoService(Processor.class),生成 META-INF 信息;
  • @SupportedAnnotationTypes({"com.example.BindView"}),声明 Processor 处理的注解,注意这是一个数组,表示可以处理多个注解;
  • @SupportedSourceVersion(SourceVersion.RELEASE_7),声明支持的源码版本

补充说明一下,@SupportedAnnotationTypes 和 @SupportedSourceVersion 必须声明,否则会报错。具体原因看大家看一下源码就明白了,这里不做过多解释。

除了注解方式,你也可以通过重写下面两个函数实现:

@AutoService(Processor.class)
public class ViewInjectProcessor extends AbstractProcessor { @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotationTypes = new HashSet<>(); annotationTypes.add("com.example.BindView"); return annotationTypes; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.RELEASE_7; } } 

3、实现 AbstractProcessor 的 process() 方法:

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { // 1、 collectInfo(roundEnvironment); // 2、 writeToFile(); return true; } 

process() 方法的实现,分为两个步骤:

  1. 收集 Class 内的所有被 @BindView 注解的成员变量;
  2. 根据上一步收集的内容,生成 .java 源文件。

为此,我们声明了两个 Map,用于保存 collectInfo() 收集的相关信息,Map 的 key 为类的全路径:

// 存放同一个Class下的所有注解信息
Map<String, List<VariableInfo>> classMap = new HashMap<>(); // 存放Class对应的信息:TypeElement Map<String, TypeElement> classTypeElement = new HashMap<>(); 

VariableInfo 是一个简单的类,用于保存被注解 View 对应的一些信息:

public class VariableInfo { // 被注解 View 的 Id 值 int viewId; // 被注解 View 的信息:变量名称、类型 VariableElement variableElement; // ... } 

4、实现 collectInfo() 方法:

void collectInfo(RoundEnvironment roundEnvironment) { classMap.clear(); classTypeElement.clear(); Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class); for (Element element : elements) { // 获取 BindView 注解的值 int viewId = element.getAnnotation(BindView.class).value(); // 代表被注解的元素 VariableElement variableElement = (VariableElement) element; // 备注解元素所在的Class TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement(); // Class的完整路径 String classFullName = typeElement.getQualifiedName().toString(); // 收集Class中所有被注解的元素 List<VariableInfo> variableList = classMap.get(classFullName); if (variableList == null) { variableList = new ArrayList<>(); classMap.put(classFullName, variableList); // 保存Class对应要素(名称、完整路径等) classTypeElement.put(classFullName, typeElement); } VariableInfo variableInfo = new VariableInfo(); variableInfo.setVariableElement(variableElement); variableInfo.setViewId(viewId); variableList.add(variableInfo); } } 

代码的注释已经很完整,这里不再说明了。

这里提一下 Element 这个元素,它的子类我们用到了以下两个:

Element
- VariableElement:代表变量
- TypeElement:代表 class

5、实现 writeToFile() 方法:

void writeToFile() {
    try {
        for (String classFullName : classMap.keySet()) {
            TypeElement typeElement = classTypeElement.get(classFullName);

            // 使用构造函数绑定数据
            MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC) .addParameter(ParameterSpec.builder(TypeName.get(typeElement.asType()), "activity").build()); List<VariableInfo> variableList = classMap.get(classFullName); for (VariableInfo variableInfo : variableList) { VariableElement variableElement = variableInfo.getVariableElement(); // 变量名称(比如:TextView tv 的 tv) String variableName = variableElement.getSimpleName().toString(); // 变量类型的完整类路径(比如:android.widget.TextView) String variableFullName = variableElement.asType().toString(); // 在构造方法中增加赋值语句,例如:activity.tv = (android.widget.TextView)activity.findViewById(215334); constructor.addStatement("activity.$L=($L)activity.findViewById($L)", variableName, variableFullName, variableInfo.getViewId()); } // 构建Class TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName() + "$$ViewInjector") .addModifiers(Modifier.PUBLIC) .addMethod(constructor.build()) .build(); // 与目标Class放在同一个包下,解决Class属性的可访问性 String packageFullName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString(); JavaFile javaFile = JavaFile.builder(packageFullName, typeSpec) .build(); // 生成class文件 javaFile.writeTo(filer); } } catch (Exception ex) { ex.printStackTrace(); } } 

这段代码主要就是通过 javapoet 来生成 .java 源文件,大家如果感觉陌生,建议先看一下 javapoet 的使用,参考文章: JavaPoet的基本使用

当然,你完全可以通过拼接字符串来生成 .java 源文件的内容。

还记得文章开头提到的 “XXX$$ViewInjector” 吗?writeToFile() 方法,就是为了生成这个 .java 源文件的。

第三步:使用 annotationProcessor

在 app 的 build.gradle 文件中,使用 APT:

dependencies {
    // ...
    annotationProcessor project(':lib-compiler') } 

lib-compiler:为第二步新建的 Java Library。

第四步:Activity 中使用 @BindView

文章开始已经演示过相关代码了,这里不再贴了。

一共分为以下两步:

  1. @BindView 注解相关 View;
  2. 调用 InjectHelper.inject(this) 方法。

然后,你尝试去编译项目,会发现 APT 为我们自动生成了 XXX$$ViewInjector.class 文件。

你可以在 app/build/intermediates/classes/debug(release、其他 buildType) 下对应的包中找到。



posted on 2019-07-12 10:33  ggband 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/ggband/p/11174599.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值