0%

APT简介及应用

什么是APT

APT是annotation processing Tools的缩写意思是注解处理工具,常用于生成模版代码来简化代码

如何自定义注解处理器

下面参考butterknife的BindView来自定义注解处理器,butterknife是jakewharton大神开发的用于android简化view初始化的库,原理是使用apt来解析注解生成辅助类自动绑定view。

工程创建

通常加上主工程需要创建4个模块

app:主工程

bindview-annotations:需要被注解处理器处理的注解聚合模块

bindview-compiler:注解处理器模块

bindview:为上层应用提供api的模块,例如ViewBinder.bind(this)

工程目录如下

工程模块创建完以后接下来做什么,重新回归我们的目的是什么,我现在要做的是实现一个view自动绑定的功能,消除findviewById模版代码,实现原理是声明绑定注解通过注解处理器获取注解以及绑定的viewId生成view绑定辅助类,辅助类中包含view绑定逻辑。在需要绑定的Actiivty种调用辅助类的绑定方法进行view自动绑定。下面就需要将原理变为代码实现

声明注解

在当前工程中创建一个java or kotlin library,声明需要注解处理器处理的注解,包括注解使用的目标类型这里是字段Field,以及注解保留策略这里的场景需要字节码文件中保留,可以在编译时获取

声明注解处理器

新建一个java or kotlin library命名为bindview-compiler并创建自定义注解处理器BindViewProcesor

根据前面工程创建部分最后面原理部分介绍,接下来需要在注解处理器中编写注解处理器逻辑

  1. 配置注解处理器支持的java源代码版本

    @Override
    public SourceVersion getSupportedSourceVersion() {
    return processingEnvironment.getSourceVersion();
    }
  2. 配置可处理的注解集合

    @Override
    public Set<String> getSupportedAnnotationTypes() {
    HashSet<String> supportAnnotationTypes = new HashSet<>();
    supportAnnotationTypes.add(BindView.class.getCanonicalName());
    return supportAnnotationTypes;
    }
  3. 初始化工具类

    /**
    *
    * @param processingEnv 提供了一些工具集合
    */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    this.processingEnvironment = processingEnv;
    filer = processingEnv.getFiler();
    }
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    environment = roundEnvironment;
    Messager messager = processingEnvironment.getMessager();
    }

    需要获取的工具类有:

    ProcessingEnvironment:提供了一些工具集合,这里用于获取日志工具,注解处理器支持的java源代码版本以及文件生成器

    RoundEnvironment:用于查询支持注解处理器处理的注解

  4. 编写注解处理器逻辑

    由于注解处理器需要引用我们声明的自定义注解,因此注解处理器模块要依赖注解模块,在注解处理器的build.gradle.kts文件中添加依赖关系

    dependencies{
    implementation(project(":bindview-annotations"))
    }

    按以下步骤实现注解处理器逻辑(在process方法中)

    • 定义注解处理器

      public class BindViewProcessor extends AbstractProcessor {
      RoundEnvironment environment;
      ProcessingEnvironment processingEnvironment;
      private Filer filer;

      /**
      * 注解处理器初始化
      * @param processingEnv 提供了一些工具集合
      */
      @Override
      public synchronized void init(ProcessingEnvironment processingEnv) {
      super.init(processingEnv);
      this.processingEnvironment = processingEnv;
      filer = processingEnv.getFiler();
      }

      @Override
      public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
      // 在这里编写注解处理器逻辑
      }
      }
    • 注册注解处理器

      注解处理器发挥作用还需要能够被编译器能够检测到,需要注册注解处理器

      在bindview-compiler模块的src/main/resources目录下创建META-INF/services目录并在这个目录下创建javax.annotation.processing.Processor文件,在文件中声明注解处理器完全限定名称(全路径名)

    • 扫描所有的添加了BindView注解的Element

      Set<? extends Element> elementsAnnotated = roundEnvironment.getElementsAnnotatedWith(BindView.class);
    • 按照Activity进行分组

      HashMap<String, List<VariableElement>> elementMap =  new HashMap<>();
      for (Element element : elementsAnnotated) {
      if (element.getKind() == ElementKind.FIELD) {
      messager.printMessage(Diagnostic.Kind.NOTE, "element:" + element);
      }
      // 因为BindView是添加在字段上面的所以这里可以强转
      VariableElement variableElement = (VariableElement)element;

      // 获取注解字段外层结构最接近的Element其实也就是Activity类型的Element强转为TypeElement
      // 目的是获取Activity的全路径名称
      TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement();
      String activityName = enclosingElement.getQualifiedName().toString();

      // 控制台打印日志
      messager.printMessage(Diagnostic.Kind.NOTE,"activityName:"+activityName);

      // 按照Activity进行分组
      List<VariableElement> elements = elementMap.get(activityName);
      if(elements == null){
      elements = new ArrayList<>();
      elementMap.put(activityName,elements);
      }
      elements.add(variableElement);
      }
    • 生成辅助类

      1. 借助Filer按照指定规则生成java源代码类文件,我们现在要生产view绑定辅助类比如MainActivityBinding,代码逻辑如下

        // 生成xxxBinding辅助类代码
        // 生成规则是Activity名称为前缀拼接Binding后缀生成辅助类
        // 辅助类中生成绑定view的代码
        for(Map.Entry<String,List<VariableElement>> entry:elementMap.entrySet()){
        String activityName = entry.getKey();
        List<VariableElement> variableList = entry.getValue();
        TypeElement activityElement = (TypeElement) variableList.get(0).getEnclosingElement();
        String packageName = processingEnv.getElementUtils().getPackageOf(activityElement).toString();
        messager.printMessage(Diagnostic.Kind.NOTE,"packageName:"+packageName);
        messager.printMessage(Diagnostic.Kind.NOTE,"activityName:"+activityName);
        String simpleName = activityElement.getSimpleName().toString();
        String className = simpleName+ConstantValues.SUFFIX_BINDING_CLASS;
        messager.printMessage(Diagnostic.Kind.NOTE,"binding class:"+className);
        Writer writer = null;
        try {
        JavaFileObject javaFileObject = filer.createSourceFile(className);
        writer = javaFileObject.openWriter();
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("package "+packageName+";\n");
        stringBuffer.append("import com.hezd.bindview.IBinder;\n");
        stringBuffer.append("public class "+className+" implements IBinder<"+activityName+">{\n");
        stringBuffer.append("\tpublic void bind("+activityName+" target) {\n");
        for(VariableElement variableElement:variableList){
        String variableName = variableElement.getSimpleName().toString();
        int resourceId = variableElement.getAnnotation(BindView.class).value();
        stringBuffer.append("\t\ttarget."+variableName+"=target.findViewById("+resourceId+");\n");
        }
        stringBuffer.append("\t}\n");
        stringBuffer.append("}\n");
        writer.write(stringBuffer.toString());
        } catch (IOException e) {
        throw new RuntimeException(e);
        }finally {
        if(writer!=null){
        try {
        writer.close();
        } catch (IOException e) {
        throw new RuntimeException(e);
        }
        }
        }
        }

        这里为了简化反射调用让辅助类实现IBinder接口这样只需要一次反射拿到IBinder实例调用bind方法即可

        public interface IBinder<T> {
        void bind(T target);
        }

提供外部调用接口的SDK

生成辅助类后需要让上层应用调用辅助类对外接口,例如这里需要让app调用到MainActivityBinding类中的bind方法进行view的绑定,还需要创建一个提供对外接口调用的模块

这里创建一个andorid library命名为bindview

因为用到bindview-compiler模块中的常量类,所以需要bindview模块需要建立依赖关系,另外还需要对外提供注解BindView访问需要添加bindview-annotations的传递依赖

dependencies {
//...
implementation(project(":bindview-compiler"))
api(project(":bindview-annotations"))
//...
}

当前模块提供辅助类访问对象ViewBinder

public class ViewBinder {
public static void bind(Object target){
String targetName = target.getClass().getCanonicalName();
String className = targetName+ ConstantValues.SUFFIX_BINDING_CLASS;
try {
Class bindgClass = Class.forName(className);
if(IBinder.class.isAssignableFrom(bindgClass)){
IBinder binder = (IBinder)bindgClass.newInstance();
binder.bind(target);
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}

使用注解处理器

  1. 引入注解处理器

    在app主工程模块引入注解处理器

    dependencies {
    //...
    annotationProcessor(project(":bindview-compiler"))
    //...
    }
  2. 引入sdk模块

    dependencies {
    //...
    implementation(project(":bindview"))
    annotationProcessor(project(":bindview-compiler"))
    //...
    }

3.在Activity中view字段添加BindView注解

public class MainActivity extends AppCompatActivity {

@BindView(R.id.textview)
public TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBinder.bind(this);

getWindow().getDecorView().postDelayed(() -> {
textView.setText("bind success");
}, 2000);

}
}

使用BindView注解view字段,然后调用ViewBinder.bind(this)实现了view自动绑定,消除了findviewbyid模版代码

4.clean然后rebuild后可以在build目录中找到注解处理器生成的MainActivityBinding

5.运行后可看到页面先显示默认Helloworld文本,两秒后被修改为bind success

写在最后

这篇文章介绍了什么是注解处理器,以及用注解处理器简单实现view自动绑定,另外还有一些可以优化的点比如可以使用google的auto-service免除手动注册注解处理器,使用javapoet库简化java代码生成,从而提高开发效率。

代码仓库地址:codelab-android-apt