Android 中的注解处理
基本语法
注解有时候也称为「元编程」,它是对代码本身进行编码,让程序自动生成代码。这种理解有一点狭义,注解不一点要生成代码,也可能只是给其他代码或 IDE 作为参考。例如 @Deprecated
就是很好的例子。
在 java 中,定义注解语法如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAnnotation {
int value();
String name() default "unknown";
}
一般情况下应该给注解加上两个维度的限制:
@Retention
: 限制注解的有效时间。SOURCE
是只在编译过程中有效,不会出现在字节码中。CLASS
会把注解编译到字节码中,但不会载入 runtime。RUNTIME
顾名思义,在运行时也可以读取。@Target
: 指明注解的作用域。例如限制为只能用于字段,或只能用于方法等。
定义注解的关键字是 @interface
,所以注解内部的写法也和接口有一点类似。不同的是,注解内部的方法会被解析为字段与参数使用。上面的代码使用起来是这样的:
@MyAnnotation(20, name="Chenhe")
Object obj;
value()
是一个特殊的方法,在使用是不需要传递参数名,而其他方法都必须写上名字来赋值。
注解只定义没有用,还需要去处理。处理注解的代码就是普通的 java 代码了。
手写 butterknife
butterknife 是一个用于 Android 的注解工具,主要作用是简化 findViewById()
操作。基本用法如下:
@BindView(R.id.textView) TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Binding.bind(this);
}
butterknife
不算是依赖注入。View 的值依然是 Activity 内部处理的,butterknife 只是简化这个过程。严格意义上的依赖注入,值是外部提供的。例如构造参数可以视为一种手动的依赖注入。
我们就手写一下这个功能来熟悉注解处理的使用。
先定义出我们的注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
下面开始写具体处理逻辑。
基本反射实现
用反射处理注解是最简单的一种。
写法非常粗暴:取得 Activity 的所有字段,筛选出带有 BindView
注解的那些,调用 Android API 取得 View 赋值给它们。
public class Binding {
public static void bind(Activity activity) {
for (Field field : activity.getClass().getDeclaredFields()) {
BindView bv = field.getAnnotation(BindView.class);
if (bv != null) {
field.setAccessible(true);
try {
field.set(activity, activity.findViewById(bv.value()));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
可惜反射存在一定的性能问题,至少,从感觉上来说存在性能问题。其实就一般用例而言,我们这种写法问题不大。但既然这是一个 lib,就得做好面对各种使用场合的准备,如果恰好某个 Activity View 比较多的话,这么反射就不合适了。
Annotation Processor
宏观原理
上面基本的反射实现问题在于如果 View 变多,就需要多次反射,影响速度。现在我们打算给每一个 Activity 创建一个对应的 ViewBinding
类,在这个类中以代码的形式(而不是反射)写上所有的 View 与 findViewById()
方法。在真正的 Activity 中实例化这个帮助类就能完成绑定。
例如针对 MainActivity
,创建帮助类 MainActivityViewBinding
如下:
// MainActivityViewBinding.java
public class MainActivityViewBinding {
public MainActivityViewBinding(MainActivity activity) {
activity.textView = activity.findViewById(2131230959);
activity.layout = activity.findViewById(2131230864);
}
}
那么在 MainActivity
中只需要这么写:
// MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MainActivityViewBinding(this); // 完成绑定
}
Annotation Processor 内要做的事情就是自动生成这些 ViewBinding 类,也就是一开始所说的「代码生成」。
项目结构
首先需要一个 Android Lib Module 编写 Binding
入口方法。
还需要一个 Java Lib Module 编写 Annotation Processor。
上面的两个模块都需要用到注解定义本身。注意依赖中的两个限制:
- Java module 不能依赖 Android module。
annotationProcessor
依赖只能执行注解处理器,不能依赖普通代码。
为了便于使用,开发就相对麻烦一点,这里创建三个模块:
- Java Lib
lib-annotations
存放注解定义。 - Java Lib
lib-processor
依赖lib-annotations
,编写注解处理器。 注意注解处理器仅支持打包成 jar 不支持 aar,所以必须是 java moudle。 - Android Lib
lib
传递依赖lib-annotations
,编写入口函数。
最终使用时,添加两个依赖就好了:
implementation project(':lib')
annotationProcessor project(':lib-processor')
注解定义和基本反射实现一样的,不赘述了。
代码生成
编写注解处理器有两步:
- 继承
AbstractProcessor
类实现注解处理器。 - 添加资源文件描述,使其他项目能识别我们的处理器。
这一步写法固定,不需要记住,需要的时候翻一翻笔记就行了。具体来讲就是创建一个文件:
./src/main/resources/META-INF/services/javax.annotation.processing.Processor
,里面直接写上实现的类。
注解处理器一般至少需要覆盖三个方法:
init()
: 初始化方法。例如取得Filer
。getSupportedAnnotationTypes()
: 返回要处理的注解的名称。process()
处理注解。
也可以给处理器添加
@SupportedAnnotationTypes
注解来指明要处理的注解类型,但这种方法不能通过class
自动获取了。所以我们选择覆盖getSupportedAnnotationTypes()
方法。
代码生成方法有很多,最粗暴的就是自己构建字符串,再写到 xxx.java 文件,显然这不是个好办法。java 原生提供了 Filer
类帮助找到合适的文件存放位置。配合 square 的开源库 javapoet 进一步简化代码生成,记得添加依赖哦。
process()
方法签名如下:
boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
set
参数是本轮处理所包含的请求处理的注解类型。注意,是注解类型,不是注解实例。这意味着多次使用注解这个集合里只会有一个。 这个参数不是很常用。
roundEnvironment
是本轮处理的所有上下文数据。
处理注解的过程中生成的代码可能包含注解,所以注解处理有多轮。
要生成代码,需要以下数据:
- 使用注解所在的包名。
- 使用注解所在的类名。
- 使用注解的变量和注解实例。
roundEnvironment.getRootElements()
可拿到所有根 Element。这里 Element
代表的是代码层面的元素,比如包、类、接口、方法、字段等。所谓根,就是本轮所有用到注解的地方会按照位置聚类,返回顶层元素。在这个用例中应该是各个 Activity Class(此处「顶层」不会返回包级别)。
拿到 Activity 的 Element,它的父级就是 Package。子集就包含各个字段。整体实现如下:
public class BindingProcessor extends AbstractProcessor {
private Filer filer;
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(BindView.class.getCanonicalName());
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
for (Element e : roundEnvironment.getRootElements()) {
String pkgName = e.getEnclosingElement().toString(); // class 的父级是 package
String clsName = e.getSimpleName().toString();
ClassName genCls = ClassName.get(pkgName, clsName + "ViewBinding"); // 要生成的类名
MethodSpec.Builder builder = MethodSpec.constructorBuilder() // 创建一个构造方法
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(pkgName, clsName), "activity"); // 参数就是当前 Activity
boolean hasBinding = false;
for (Element child : e.getEnclosedElements()) {
if (child.getKind() != ElementKind.FIELD) // 只处理字段元素
continue;
BindView anno = child.getAnnotation(BindView.class);
if (anno == null)
continue;
hasBinding = true;
builder.addStatement("activity.$N = activity.findViewById($L)", child.getSimpleName(), anno.value()); // 生成 findViewById 方法调用
}
if (hasBinding) {
TypeSpec builtClass = TypeSpec.classBuilder(genCls) // 要生成的类
.addModifiers(Modifier.PUBLIC)
.addMethod(builder.build())
.build();
try {
JavaFile.builder(pkgName, builtClass)
.build().writeTo(filer); // 写入文件
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
return true; // 不再允许其他注解处理器z
}
}
这个实现仅作为演示,并不完善。例如把
@BindView
写在奇奇怪怪的地方可能会出问题。
在 App 项目中引用我们的注解处理器项目,构建一下(只 sync 是不行的),就能找到生成的文件:
// ./app/build/generated/ap_generated_sources/debug/out/.../MainActivityViewBinding.java
// 这些代码是自动生成的
public class MainActivityViewBinding {
public MainActivityViewBinding(MainActivity activity) {
activity.textView = activity.findViewById(2131230959);
activity.layout = activity.findViewById(2131230864);
}
}
调用帮助类
帮助类已经自动生成了,下面要调用它才行。每一个 Activity 都有自己的帮助类,为了正确调用依然得使用反射。但这么用几乎不会影响性能,因为只在 Activity 创建时反射一次,无论内部有多少个 View 都无需反射,这点耗时是可以接受的。
public class Binding {
public static void bind(Activity activity) {
try {
// 利用反射找到生成的类
Class<?> bindingClass = Class.forName(activity.getClass().getCanonicalName() + "ViewBinding");
// 找到构造函数来实例化
Constructor<?> constructor = bindingClass.getDeclaredConstructor(activity.getClass());
constructor.newInstance(activity);
} catch (ClassNotFoundException | InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
这个 Demo 到此就完成了,和 butterknife 使用方法一致。