什么是APT
APT是annotation processing Tools的缩写意思是注解处理工具,常用于生成模版代码来简化代码
如何自定义注解处理器
下面参考butterknife的BindView来自定义注解处理器,butterknife是jakewharton大神开发的用于android简化view初始化的库,原理是使用apt来解析注解生成辅助类自动绑定view。
工程创建
通常加上主工程需要创建4个模块
app:主工程
bindview-annotations:需要被注解处理器处理的注解聚合模块
bindview-compiler:注解处理器模块
bindview:为上层应用提供api的模块,例如ViewBinder.bind(this)
工程目录如下
![](https://github.com/hezd/Image-Folders/blob/master/codelab-android-apt/%E5%B7%A5%E7%A8%8B%E7%9B%AE%E5%BD%95.jpg?raw=true)
工程模块创建完以后接下来做什么,重新回归我们的目的是什么,我现在要做的是实现一个view自动绑定的功能,消除findviewById模版代码,实现原理是声明绑定注解通过注解处理器获取注解以及绑定的viewId生成view绑定辅助类,辅助类中包含view绑定逻辑。在需要绑定的Actiivty种调用辅助类的绑定方法进行view自动绑定。下面就需要将原理变为代码实现
声明注解
在当前工程中创建一个java or kotlin library,声明需要注解处理器处理的注解,包括注解使用的目标类型这里是字段Field,以及注解保留策略这里的场景需要字节码文件中保留,可以在编译时获取
![](https://github.com/hezd/Image-Folders/blob/master/codelab-android-apt/%E5%A3%B0%E6%98%8E%E6%B3%A8%E8%A7%A3.jpg?raw=true)
声明注解处理器
新建一个java or kotlin library命名为bindview-compiler并创建自定义注解处理器BindViewProcesor
根据前面工程创建部分最后面原理部分介绍,接下来需要在注解处理器中编写注解处理器逻辑
配置注解处理器支持的java源代码版本
public SourceVersion getSupportedSourceVersion() {
return processingEnvironment.getSourceVersion();
}配置可处理的注解集合
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> supportAnnotationTypes = new HashSet<>();
supportAnnotationTypes.add(BindView.class.getCanonicalName());
return supportAnnotationTypes;
}初始化工具类
/**
*
* @param processingEnv 提供了一些工具集合
*/
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.processingEnvironment = processingEnv;
filer = processingEnv.getFiler();
}
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
environment = roundEnvironment;
Messager messager = processingEnvironment.getMessager();
}需要获取的工具类有:
ProcessingEnvironment:提供了一些工具集合,这里用于获取日志工具,注解处理器支持的java源代码版本以及文件生成器
RoundEnvironment:用于查询支持注解处理器处理的注解
编写注解处理器逻辑
由于注解处理器需要引用我们声明的自定义注解,因此注解处理器模块要依赖注解模块,在注解处理器的build.gradle.kts文件中添加依赖关系
dependencies{
implementation(project(":bindview-annotations"))
}按以下步骤实现注解处理器逻辑(在process方法中)
定义注解处理器
public class BindViewProcessor extends AbstractProcessor {
RoundEnvironment environment;
ProcessingEnvironment processingEnvironment;
private Filer filer;
/**
* 注解处理器初始化
* @param processingEnv 提供了一些工具集合
*/
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.processingEnvironment = processingEnv;
filer = processingEnv.getFiler();
}
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);
}生成辅助类
借助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 { |
当前模块提供辅助类访问对象ViewBinder
public class ViewBinder { |
使用注解处理器
引入注解处理器
在app主工程模块引入注解处理器
dependencies {
//...
annotationProcessor(project(":bindview-compiler"))
//...
}引入sdk模块
dependencies {
//...
implementation(project(":bindview"))
annotationProcessor(project(":bindview-compiler"))
//...
}
3.在Activity中view字段添加BindView注解
public class MainActivity extends AppCompatActivity { |
使用BindView注解view字段,然后调用ViewBinder.bind(this)实现了view自动绑定,消除了findviewbyid模版代码
4.clean然后rebuild后可以在build目录中找到注解处理器生成的MainActivityBinding
5.运行后可看到页面先显示默认Helloworld文本,两秒后被修改为bind success
![](https://github.com/hezd/Image-Folders/blob/master/codelab-android-apt/%E8%BF%90%E8%A1%8C%E7%BB%93%E6%9E%9C.jpg?raw=true)
写在最后
这篇文章介绍了什么是注解处理器,以及用注解处理器简单实现view自动绑定,另外还有一些可以优化的点比如可以使用google的auto-service免除手动注册注解处理器,使用javapoet库简化java代码生成,从而提高开发效率。
代码仓库地址:codelab-android-apt