APT与ButterKnife

本文主要介绍APT技术与ButterKnife的源码简析。

注解处理器(Annotation Processor)

注解

关于注解这里不做介绍,参考这篇文章:简单介绍 Java 中的注解 (Annotation)

注解处理器

如果你对注解有一定了解的话,应该知道我们在运行时(Runtime)获取注解信息的方式是反射。如果我们想要在编译期(Compile time)就获取到注解的信息,那么我们可以使用注解处理器来做这样的工作。注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以对自定义注解,并注册相应的注解处理器。

一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。

注解处理器工作流

Java代码编译的过程主要包括三步,解析、注解处理、字节码生成,具体的流程见下图。

image-20180705201121053

这里需要循环的处理注解,原因是注解处理器可能会生成新的Java文件,这些新的Java文件也要进行一遍语法分析,并且有可能这些新的Java文件也包含注解,还要继续被注解处理器分析处理。

JavaPoet

前面提到注解处理这一步可以生成Java文件,但是实际上注解处理器只提供了注解以及与其相关的Token信息,还有一个Java文件的输出流。你可以直接通过这些必要的信息,将代码以字符流的形式写入Java文件,但是这样的过程无疑是吃力不讨好的(代码编写烦琐且难以维护)。

JavaPoet是square开源的一个库,作用就如其名,让你在这个过程,像写诗一样,优雅的将Java代码字符流写入Java文件。

使用的细节不再赘述,可以直接参考官方文档:square/javapoet/README.md

实现一个简单的ViewBinder

上一节是从概念上介绍了一下注解处理器,这一节主要是介绍通过注解处理器实现一个类似于ButterKnife的 @BindView功能的注解。具体的实现参考:xybean/MiniViewBinder

自定义注解处理器

1、module设计

项目的module最好分为三个:

  • 一个纯Java模块,命名为viewbinder-annotations,用于放置需要开放给外部的Annotation。
  • 一个纯Java模块,命名为viewbinder-compiler,用于放置注解处理器的实现。
  • 一个Android模块,用于使用生成的类,并开放接口给外部使用(外部不直接使用生成类)。

2、添加依赖

viewbinder-compiler模块需要添加依赖

1
2
3
4
compile 'com.squareup:javapoet:1.10.0'
// auto.service 用于生成一些文件,自动将自定义注解处理器注册到javac中
compile 'com.google.auto.service:auto-service:1.0-rc4'
implementation project(':viewbinder-annotations')

3、实现注解

在viewbinder-annotations添加注解实现,注意元注解的值。

1
2
3
4
5
6
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
@Documented
public @interface BindView {
int viewId() default -1;
}

4、实现注解处理器

实现注解处理器只要继承AbstractProcessor并将其注册到编译器即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 将当前注解处理器注册到编译器中
@AutoService(Processor.class)
public class ViewBinderProcessor extends AbstractProcessor {

// 可以利用Messager将日志输出到控制台
private Messager messager;
// Java代码的输出流
private Filer filer;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// processingEnv表示了当前注解处理的上下文,由它可以获取到一些必要的工具和信息
messager = processingEnv.getMessager();
filer = processingEnv.getFiler();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
// 告诉当前注解处理器它需要处理的注解
annotations.add(BindView.class.getCanonicalName());
return annotations;
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment)
// 获取注解及其相关信息,利用JavaPoet生成类
return true;
}
}

5、注解处理器的调试

参考文章: Android studio 下调试注解处理器

注意在调试前先启动自己设置的远程调试器,然后再build项目。

生成类

生成类的思路是,在ViewBinderProcessor.process()中去的注解字段以及其所在类的信息,然后根据这些信息生成XXX__ViewBinder.java。这里的生成类的名字是我们自定义的,需要满足一定的生成规则,这样才能在viewbinder模块中被正确调用。

在处理过程中,会缓存注解相关信息,并且处理器会被多次调用,使用的是同一个处理器实例,因此需要在每次处理完后清空这些缓存的数据,以免重复生成文件导致错误。

使用生成类

在实现了注解处理器,并将注解使用在主项目中后,点击build按钮,就会在主项目的build/generated/res/source/apt目录下生成我们的生成类。

如果要使用生成类,需要在viewbinder模块中使用反射调用,可见生成类的命名规范性是非常重要的。

ButterKnife源码简析

这一节的源码分析主要是分析注解处理的逻辑与View绑定的流程。

总体流程图
解析注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 以ButterKnifeProcessor.process()为入口
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// 建立类(TypeElement)与类中注解相关信息(BindingSet)的映射关系
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
// 取出相关的信息用于生成目标类
JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}

关键的解析过程实现在findAndParseTargets()中,它对不同类型的注解分别作了处理,然后汇总所有的解析结果到Map<TypeElement, BindingSet>中。这里我们只关注BindView相关的实现,也就是方法parseBindView

的实现。

这个方法的大致可以概括为:

image-20180708153746864

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 做过简化处理的源代码,实际实现处理细节会更多
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 校验访问权限与注解是否用在了android或者java包中
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
|| isBindingInWrongPackage(BindView.class, element);
// Verify that the target type extends from View.
TypeMirror elementType = element.asType();
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
return;
}
// 从注解中取出资源id值
int id = element.getAnnotation(BindView.class).value();
Id resourceId = elementToId(element, BindView.class, id);
// 这里省略了去重检查,将从注解中取出的信息封装成BindingSet.Builder存入builderMap
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
String name = simpleName.toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
builder.addField(resourceId, new FieldViewBinding(name, type, required));
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement);
}
生成类

生成类的主要逻辑在BindingSet.createType()中:

1
2
3
4
5
6
7
8
9
10
11
12
private TypeSpec createType(int sdk, boolean debuggable, boolean useAndroidX) {
// ...
if (isView) {
result.addMethod(createBindingConstructorForView(useAndroidX));
} else if (isActivity) {
result.addMethod(createBindingConstructorForActivity(useAndroidX));
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog(useAndroidX));
}
// ...
return result.build();
}

生成类的关键在于针对注解所在类的不同做不同的处理,具体的看生成类的样式就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 如果是Activity
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;

@UiThread
public MainActivity_ViewBinding(MainActivity target) {
// 关键点在于这里取得父View的方式不同
this(target, target.getWindow().getDecorView());
}

@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;
// 这个方法会根据id值从父View中取出所需的子view
target.btn = Utils.findRequiredViewAsType(source, R.id.btn, "field 'btn'",
}
}
1
2
3
4
5
6
7
8
9
10
11
// 如果是View
public class CustomView_ViewBinding implements Unbinder {
private CustomView target;

@UiThread
public CustomView_ViewBinding(CustomView target) {
// 关键点在于这里取得父View的方式不同
this(target, target);
}
// 省略一些一样的模板方法
}
1
2
3
4
5
6
7
8
9
10
11
// 如果是Dialog
public class CustomDialog_ViewBinding implements Unbinder {
private CustomDialog target;

@UiThread
public CustomDialog_ViewBinding(CustomDialog target) {
// 关键点在于这里取得父View的方式不同
this(target, target.getWindow().getDecorView());
}
// 省略一些一样的模板方法
}
使用生成类

使用生成类的方法在ButterKnife.createBinding()中,具体的逻辑就是查找到的输入对象target对应的生成类,然后调用生成类的构造方法实现注入。

1
2
3
4
5
6
7
8
9
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
// 调用构造方法,从上一节生成类可以知道,注入的实现都在构造方法内
return constructor.newInstance(target, source);
}

这里的关键实现在于如何找到对应的生成类,查看方法findBindingConstructorForClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
// 从缓存中取得生成类
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
return bindingCtor;
}
try {
// 加载生成类
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} catch (ClassNotFoundException e) {
// 递归查找输入类父类的生成类
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
// 将查询结果放入缓存
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}

参考资料

简单介绍 Java 中的注解 (Annotation)

square/javapoet

Java注解处理器