ButterKnife解析

JakeWharton的黄油刀ButterKnife,用过的人都说好。在复杂的页面中用上它不知道能为你省去多少样板代码,代码质量看起来就提高了一个档次! 这么厉害的手段不研究一下怎么行,赶紧来看看。

前言

首先介绍一些预备知识。

注解的解析方式

通过元注解@Retention来标识,有三种:

  • RetentionPolicy.SOURCE: 源码级别解析,例如@Override, @SupportWarnings。这类注解在编译成功后就不会再起作用,并且不会出现在.class文件中。
  • RetentionPolicy.CLASS: 编译时解析,默认的解析方式,会保留在最终的.class中,但是无法在运行时获取。
  • RetentionPolicy.RUNTIME: 运行时解析,会保留在最终的.class文件中。这类注解可以用反射API中的getAnnotations()获取到。

更多注解基础知识可以参考codekk公共技术点

编译时解析

编译时解析是注解强大的地方之一。你可以利用它在编译时帮你生成一些代码逻辑,避免了运行时利用反射解析所带来的性能开销。那么问题来了,注解是怎么在编译的时候被解析的呢?

Java 5带有Annotation Processing Tool(APT)。它能够提供一个编译时的注解处理,并且能够产生新的代码与文件,同时能够让java编译器将生成的代码和原来的代码一起编译!与之配套的还有Mirror API,它提供在编译时对程序结构的静态、只读的分析。这个功能很强大,但是处理起来略显麻烦。Java 6开始将这个功能整合进编译器中,你只要继承AbstractProcessor,并在javac中通过参数-processor指定注解处理就好了。

当然你也可以不指定具体的类,在META-INF/service/下创建文件javax.annotation.processing.Processor,在里面指定类名(全名)。Java的ServiceLoader会自己去找到这个类并编译。

说到这里,不得不说的两个库:

  • google-auto-service: 只要在你的Processor类上加上注解@AutoService(Processor.class),它自动帮你将这个类添加到META-INF/service/javax.naaotation.processing.Processor下,非常好用。
  • android-apt, 在gradle插件中整合了-processor-processorPath命令,还可以指定project,轻轻松松实现APT。

看完这段心里大概对实现原理有一个初步的猜测,那么开始挖掘代码吧!

从Bind开始

ButterKnife能够提供的注解类型太多了,本文就以解析@Bind注解为例。所有注解绑定都是通过一句ButterKnife.bind()函数开始的,我们先来看看它做了些什么

1
2
3
4
5
6
7
8
Class<?> targetClass = target.getClass();
try {
if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
viewBinder.bind(finder, target, source);
} catch (Exception e) {
throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
}

这个findViewBinderForClass(),负责找到ViewBinder,我们可以看看ViewBinder是干嘛的:

1
2
3
public interface ViewBinder<T> {
void bind(Finder finder, T target, Object source);
}

Finder是一个枚举,它的作用就是适配各类findViewByIdFinder.findView()函数上,比如你用ButterKnife.bind(Activity activity)时调用的就是bind(target, target, Finder.ACTIVITY)

可以看看Finder.ACTIVITY的声明:

1
2
3
4
5
6
7
8
9
ACTIVITY {
@Override protected View findView(Object source, int id) {
return ((Activity) source).findViewById(id);
}
@Override public Context getContext(Object source) {
return (Activity) source;
}
}

很清楚吧。那么我们话题回到ViewBinder上,你会发现全局实现它的只有一个啥也没干的类NOP_VIEW_BINDER。纳尼?好吧,我们先看看findViewBinderForClass是怎么去找的,希望从这寻求到突破点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
throws IllegalAccessException, InstantiationException {
// (省略代码)缓存中有就从缓存中取
// (省略代码)"java."与"android."开头的包中的类,返回NOP_VIEW_BINDER
try {
Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
} catch (ClassNotFoundException e) {
viewBinder = findViewBinderForClass(cls.getSuperclass());
}
//(省略代码)将viewBinder放入缓存
return viewBinder;
}

我刚开始看到这的时候心中打了一个大大的问号,啥玩意?Class.forName(clsName + "$$ViewBinder")??这是啥,好的吧,至此正常路线就看不到任何有价值的东西。那么问题留在这里,我们开始看Processor。

##真相大白 - 注解处理与JavaPoet

实际上从前言中介绍的注解处理你已经心理有个谱,就是在注解处理器里面重写一下process方法。那么JW大神是怎么生成的代码?生成什么样的代码?我们还是应该去一探究竟。

首先看一下ButterKnifeProcessor这个类,顶上一个亮闪闪的@AutoService(Processor.class),从此不用再关心Processor被javac解析的事情了。先来看看process方法里干了什么吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingClass bindingClass = entry.getValue();
try {
bindingClass.brewJava().writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
e.getMessage());
}
}
return true;
}

这个函数信息量很大,我将它拆成两部分来说:

1. 解析所有注解 - findAndParseTargets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseBind(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, Bind.class, e);
}
}
//(省略代码)解析其他类型注解,本文就以解析@Bind为例,故省略
}

这部分代码浅显易懂,解析所有的被@Bind所注解的Element(肯定是field,因为@Bind只能修饰field),并处理它。在parseBind中会初步判断Element的修饰符,不可以是privatestatic下的变量,不可以是私有类下的变量,也不可以是非类对象下的field(比如enum)。之后判断被注解的field是否List、Array,但是它们最终都会走向parseBindOne()函数(解析单个Bind)。那么我们来看看这个函数:

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
private void parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<TypeElement> erasedTargetNames) {
// (省略代码)验证element是否继承自View
// (省略代码)验证@Bind中是否至少含有一个id
// (省略代码)如果上述两个验证失败,则退出。
int id = ids[0];
// 判断element所在的类是否存在对应的BindingClass
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass != null) {
// (省略代码)判断待绑定的id已经被绑定过
} else {
// 创建一个对应类的BindingClass
bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
}
String name = element.getSimpleName().toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
// 在对应BindingClass中添加FieldViewBinding&id组合
FieldViewBinding binding = new FieldViewBinding(name, type, required);
bindingClass.addField(id, binding);
// 将绑定变量所在的类添加到待unBind序列中。
erasedTargetNames.add(enclosingElement);
}

这里出现了两个新词汇:BindingClassFieldViewBinding,我们来看看它们都是啥。

  • BindingClass:所有应用ButterKnife的类都有一个单独的BindingClass与其挂钩,它里面包含了很多信息:Field绑定、Drawable绑定、Collection绑定等等很多信息。以类名+”$$ViewBinder”、包名标识。这个东西似乎前面看见过啊!有点激动,继续往下看。
  • FieldViewBinding:存放于BindingClass内,用于绑定变量View(与之对应还有绑定Drawable、Bitmap、List、Method等)。记录每个Field的绑定信息,如变量名、类名。每个FieldViewBinding都与一个id对应。同一个id添加第二个FieldViewBinding时会在之前出现的parseBindOne()函数中报错。

如果@Bind修饰符不为空,则意味着该FieldViewBinding需要绑定。

至此@Bind的解析工作就结束了,所有的信息都以FieldViewBinding的形式存在于注解所在的类对应的BindingClass中。

2. 生成代码

再次介绍一个大杀器javapoet,square出品的生成.java文件的Java API。通过清晰的语法来生成一个文件结构,无敌。

那我们再回到原先的注解处理process()方法中。它会遍历每一个生成的BindingClass,并调用bindingClass.brewJava().writeTo(filer);。其中filer是通过ProcessingEnvironment.getFiler()获取的,它的作用在注释中说了:

Returns the filer used to create new source, class, or auxiliary files.

就是用来生成代码的,APT允许你生成新的代码并且将他们一起编译。由此看来writeTo(filer)方法就是将生成的代码结构变成.java文件,看来奥秘应该是在brewJava()这个函数中!那我们赶紧来看看!

这个函数会做几件事情:

生成一个public class {CLASS}$$ViewBinder<T extends {CLASS}> implements ViewBinder<T>的类结构。其中{CLASS}代表了ButterKnife注解所在的类名。这就解释了我们最开始百思不得其解的Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder")。同时它会在其中添加一个实现方法bind(finder, target, source),我们来看看它是怎么做的:

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
37
38
39
40
41
42
private MethodSpec createBindMethod() {
//添加函数头
MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(FINDER, "finder", FINAL)
.addParameter(TypeVariableName.get("T"), "target", FINAL)
.addParameter(Object.class, "source");
// (省略代码) 是否绑定资源
// (省略代码) 是否调用父类的bind
// If the caller requested an unbinder, we need to create an instance of it.
if (hasUnbinder()) {
final String statment;
if (parentUnbinder != null) {
// Explicitly call super in case this class has child's as well.
statment = "$T unbinder = super.accessUnbinder($N)";
} else {
statment = "$T unbinder = createUnbinder($N)";
}
result.addStatement(statment, unbinderBinding.getUnbinderClassName(), "target");
}
// 正主来了!
if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
// 声明一个View供后续调用
result.addStatement("$T view", VIEW);
// 对每一个绑定过的id,添加绑定代码
for (ViewBindings bindings : viewIdMap.values()) {
addViewBindings(result, bindings);
}
// (省略代码) List/Array的绑定
}
// (省略代码) Resource/Bitmap/Drawable的绑定
return result.build();
}

接下来计入到addViewBindings中,分作两部:找到View,将View赋值给变量。

  • 找到View:如果该ViewBinding需要绑定,则添加如下语句:
1
result.addStatement("view = finder.findRequiredView(source, $L, $S)", bindings.getId(),asHumanDescription(requiredViewBindings));

我们看看Finder.findRequiredView()里面是什么,它会添加到到时候执行的代码之中:

1
2
3
4
5
6
7
8
9
10
11
12
public <T> T findRequiredView(Object source, int id, String who) {
T view = findOptionalView(source, id, who);
if (view == null) {
//抛出异常
}
return view;
}
public <T> T findOptionalView(Object source, int id, String who) {
View view = findView(source, id);
return castView(view, id, who);
}

还记得开头出现的Finder.ACTIVITY吗!回去看看,恍然大悟有没有!绑定不同的Finder会有不同的代码加到生成的代码里去寻找View。JakeWharton真tm是个天才。

  • 赋值给原变量

View找着了之后呢,肯定要把它赋值回原来的变量里:

1
2
3
4
5
6
7
8
9
10
11
private void addFieldBindings(MethodSpec.Builder result, ViewBindings bindings) {
Collection<FieldViewBinding> fieldBindings = bindings.getFieldBindings();
for (FieldViewBinding fieldBinding : fieldBindings) {
if (fieldBinding.requiresCast()) {
result.addStatement("target.$L = finder.castView(view, $L, $S)", fieldBinding.getName(),
bindings.getId(), asHumanDescription(fieldBindings));
} else {
result.addStatement("target.$L = view", fieldBinding.getName());
}
}
}

上面这段代码已经非常简明易懂:如果需要Cast,就添加一个cast语句的赋值;否则直接赋值。

最终都会通过Javapoet生成一个文件流写入到Filer中,被javac编译。

这一切都只是通过一个注解和一句ButterKnife.bind()