开源项目源码分析——ButterKnife

前言

从 ButterKnife 开始,我会不定时研究常用Android开源项目,梳理它们各自的原理,并沉淀为技术文章,敬请期待。

正文

概要

ButterKnife 提供了很多自定义注解,最常用的就是 @BindView 和 @OnClick, 然后由自定义APT(注解处理器)在编译时生成绑定 View 和点击事件的代码。在获取类 xxx_ViewBinding 的对象时,借助了 Java 反射

源码跟踪

我阅读的是 8.2.1 版本的源码,它的使用不具体介绍了,不同的版本,注解名可能会有变动,建议直接看 GitHub 上的 ReadMe

在某个要使用 ButterKnife 的 activity 中,我们需要在 OnCreate() 方法中添加代码:

1
ButterKnife.bind(this);

进入 bind(this) 方法:

1
2
3
4
5
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}

getDecorView() 获取到 window 中的最顶层 View,然后传入 createBinding ();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
try {
return constructor.newInstance(target, source);
}
//······省略部分 try/catch 代码
}

首先获取到该 activity 的 Class 对象,将其传入 findBindingConstructorForClass():

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
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}

BINDERS 是一个 LinkedHashMap ,用来缓存 clsName_ViewBinding 的构造器对象,避免下次调用继续使用反射影响性能。若缓存中没有,通过 Class 的 getClassLoader() 加载该类,然后用反射得到构造器对象,并放入 map。

然后调用 newInstance() 生成 clsName_ViewBinding 的实例。

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
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view2131427422;
//······略
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
view = Utils.findRequiredView(source, R.id.text, "field 'textView' and method 'onViewClicked'");
target.textView = Utils.castView(view, R.id.text, "field 'textView'", TextView.class);
view2131427422 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onViewClicked(p0);
}
});
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
//解绑时释放持有的引用(view 和点击事件)
this.target = null;
target.textView = null;
view2131427422.setOnClickListener(null);
view2131427422 = null;
}
}

构造方法里的 Utils.findRequiredView 调用了 View 的 findViewById() 实现对 view 的绑定,并强制转换为声明的类型。需要注意的是,因为通过 target.xxx 来赋值,所以我们不能将 View 定义为 private 的。变量 view2131427422 是为了解绑时释放对 OnClickListener 的引用,所以该变量的数目取决于 @OnClick 中 View Id 的数目。

看到这里,我们对 ButterKnife 的核心流程有个大致了解了。但你肯定很好奇,xxx_ViewBinding 是怎么生成的?这就是 APT(注解处理器) 干的活啦。

APT 和 ButterKnifeProcessor

APT(Annotation Processing Tools) 的原理是在某些代码元素上(如类型、函数、字段等)添加注解,在编译时编译器会检查 AbstractProcessor 的子类,并且调用该类型的 process 函数,然后将添加了注解的所有元素都传递到 process 函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类,这也就是EventBus,Retrofit,Dragger等开源库的基本原理。

init(ProcessingEnvironment env)

我们先来看看 ButterKnifeProcessor 中的 init(ProcessingEnvironment env) 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
String sdk = env.getOptions().get(OPTION_SDK_INT);
if (sdk != null) {
try {
this.sdk = Integer.parseInt(sdk);
} catch (NumberFormatException e) {
env.getMessager()
.printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
+ sdk
+ "'. Falling back to API 1 support.");
}
}
// 得到一些有用的工具类
elementUtils = env.getElementUtils();
typeUtils = env.getTypeUtils();
filer = env.getFiler();
try {
trees = Trees.instance(processingEnv);
} catch (IllegalArgumentException ignored) {
}
}

init 中主要根据 env 得到一些工具类。其中的 filter 主要是用来生成 Java 代码,而 elementUtilstypeUtils 会在下面源码中用到。

getSupportedAnnotationTypes()
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 static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(
OnCheckedChanged.class,
OnClick.class,
OnEditorAction.class,
OnFocusChange.class,
OnItemClick.class,
OnItemLongClick.class,
OnItemSelected.class,
OnLongClick.class,
OnPageChange.class,
OnTextChanged.class,
OnTouch.class
);
@Override
public Set<String> getSupportedAnnotationTypes() {
// 返回注解处理器支持处理的注解
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
// 得到所有的注解
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindArray.class);
annotations.add(BindBitmap.class);
annotations.add(BindBool.class);
annotations.add(BindColor.class);
annotations.add(BindDimen.class);
annotations.add(BindDrawable.class);
annotations.add(BindFloat.class);
annotations.add(BindInt.class);
annotations.add(BindString.class);
annotations.add(BindView.class);
annotations.add(BindViews.class);
annotations.addAll(LISTENERS);
return annotations;
}

getSupportedAnnotationTypes() 方法的作用就是返回该注解处理器所支持处理的注解集合。

process(Set<? extends TypeElement> elements, RoundEnvironment env)

这是注解处理器中最重要的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// 扫描所有注解,最后生成 map
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
// 遍历 bindingMap 并且通过 Filer 生成 Java 代码
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return true;
}

process 方法干了两个活:

  1. 扫描所有的注解,然后生成以 TypeElement 为 key ,BindingSet 为 value 的 Map ;
  2. 遍历生成的 Map,通过 Filter 来生成对应的辅助类源码,这里使用了 JavaPoet 来生成 Java 源码,可以阅读这篇文章 《javapoet——让你从重复无聊的代码中解放出来》
findAndParseTargets(env)
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
// 扫描所有的ButterKnife注解,并且生成以TypeElement为键,BindingSet为值的HashMap
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
scanForRClasses(env);
// 省略一堆解析各种注解的源码,这些源码做的事情和下面这个 for 循环一样
// 所以只要看这个解析 @BindView 就够了
...
// Process each @BindView element.
// 遍历所有被 @BindView 标注的元素
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
// we don't SuperficialValidation.validateElement(element)
// so that an unresolved View type can be generated by later processing rounds
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
...
}

先来看关于 BindView 的那个 for 循环,它会遍历所有被 @BindView 注解的属性,然后调用 parseBindView方法。

parseBindView
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames) {
// 得到注解 @BindView 元素所在的类元素
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Start by verifying common generated code restrictions.
// ---------- 类型校验逻辑 start ---------------
// 判断是否被注解在属性上,如果该属性是被 private 或者 static 修饰的,则出错
// 判断是否被注解在错误的包中,若包名以“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 (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
// 判断元素是不是View及其子类或者Interface
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
if (elementType.getKind() == TypeKind.ERROR) {
note(element, "@%s field with unresolved type (%s) "
+ "must elsewhere be generated as a View or interface. (%s.%s)",
BindView.class.getSimpleName(), elementType, enclosingElement.getQualifiedName(),
element.getSimpleName());
} else {
error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
BindView.class.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
hasError = true;
}
}
// 如果有错误 不执行下面代码
if (hasError) {
return;
}
//---------------- 类型校验逻辑 end -----------------
// Assemble information on the field. //得到被注解的注解值,即 R.id.xxx
int id = element.getAnnotation(BindView.class).value();
// 根据所在的类元素去查找 builder
BindingSet.Builder builder = builderMap.get(enclosingElement);
// 如果相应的 builder 已经存在
if (builder != null) {
// 得到相对应的 View 绑定的属性名
String existingBindingName = builder.findExistingBindingName(getId(id));
// 若该属性名已经存在,则说明之前已经绑定过,会报错
if (existingBindingName != null) {
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
BindView.class.getSimpleName(), id, existingBindingName,
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
} else {
// 如果没有对应的 builder ,就通过 getOrCreateBindingBuilder 方法生成,并且放入 builderMap 中
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
// 得到注解名
String name = element.getSimpleName().toString();
// 得到注解元素的类型
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
// 根据 id ,添加相对应的 Field 的绑定信息
builder.addField(getId(id), new FieldViewBinding(name, type, required));
// Add the type-erased version to the valid binding targets set.
// 添加到待 unbind 的序列中
erasedTargetNames.add(enclosingElement);
}

parseBindView 方法中基本上都加了注释,在方法的开头会对该 element 去做校验。如果校验没通过的话,就没有下面代码的什么事了。若校验通过之后,生成该 element 所在的类元素对应的 builder ,builder 中添加相应的 Field 绑定信息,最后添加到待 unbind 的序列中去。

现在,我们回过头来看看 findAndParseTargets(env) 方法的后半段:

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
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
... // 省略前半部分源码
// Associate superclass binders with their subclass binders. This is a queue-based tree walk
// which starts at the roots (superclasses) and walks to the leafs (subclasses).
Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
new ArrayDeque<>(builderMap.entrySet());
Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
// 一个个取出遍历
Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();
// 得到对应的 key 和 value
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
// 找到该类元素的父元素
TypeElement parentType = findParentType(type, erasedTargetNames);
if (parentType == null) {
// 生成 BindingSet ,放入 Map 中
bindingMap.put(type, builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
// 设置父元素的 BindingSet
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
// 有父元素,但是父元素的 BindingSet 还没有被 build 出来,
// 所以再放入 entries 中等待遍历
entries.addLast(entry);
}
}
}
// 解析结果都会存放在 bindingMap 中
return bindingMap;
}

主要就是把之前的 builderMap 转换为了 bindingMap 并返回。到了这里,我们把 process(Set<? extends TypeElement> elements, RoundEnvironment env) 做的第一件事情搞清楚了,下面就接着来看第二件事情了。

1
2
3
4
5
6
7
8
9
10
11
12
// 遍历 bindingMap 并且通过 Filer 生成 Java 代码
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
brewJava(int sdk) 和 createType(int sdk)

从上面可以看到,遍历了之前得到的 bindingMap ,然后利用 binding 中的信息生成相应的 Java 源码。所以在 binding.brewJava(sdk) 这个方法是我们重点关注对象。那么就进入 BindingSet 中去看看吧:

1
2
3
4
5
6
JavaFile brewJava(int sdk) {
// 生成 JavaFile,添加相应的注释
return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}

就是利用了 JavaFile.builder 生成了一个 JavaFile 对象。但是我们发现其中有一个 createType(int sdk) 方法,继续跟进去看:

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
private TypeSpec createType(int sdk) {
// 生成类名为 bindingClassName 的公共类,比如 MainActivity_ViewBinding
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
// 是否修饰为 final ,默认是 false
if (isFinal) {
result.addModifiers(FINAL);
}
if (parentBinding != null) {
// 如果有父类的话,那么要继承父类
result.superclass(parentBinding.bindingClassName);
} else {
// 如果没有父类,那么实现 Unbinder 接口
result.addSuperinterface(UNBINDER);
}
// 增加一个变量名为target,类型为targetTypeName的成员变量
if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}
// 如果没有 View 绑定
if (!constructorNeedsView()) {
// Add a delegating constructor with a target type + view signature for reflective use.
// 该生成的构造方法被 @deprecated ,一般作为反射使用
result.addMethod(createBindingViewDelegateConstructor(targetTypeName));
}
// 生成构造方法,另外 findViewById 类似的代码都在这里生成
// Xxxx_ViewBinding 一般都是执行这个方法生成构造器
result.addMethod(createBindingConstructor(targetTypeName, sdk));
if (hasViewBindings() || parentBinding == null) {
//生成unBind方法
result.addMethod(createBindingUnbindMethod(result, targetTypeName));
}
return result.build();
}

createType(int sdk) 方法中,基本构建好了一个类的大概,其中对于构造器以及类似 findViewById 的操作都是在 createBindingConstructor(targetTypeName, sdk) 中实现:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
private MethodSpec createBindingConstructor(TypeName targetType, int sdk) {
// 创建构造方法,方法修饰符为 public ,并且添加注解为UiThread
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC);
// 如果有方法绑定,比如 @OnClick
if (hasMethodBindings()) {
// 如果有,那么添加 targetType 类型,final 修饰,参数名为 target 的构造方法参数
constructor.addParameter(targetType, "target", FINAL);
} else {
// 如果没有,和上面比起来就少了一个 final 修饰符
constructor.addParameter(targetType, "target");
}
// 如果有注解的 View
if (constructorNeedsView()) {
// 那么添加 View source 参数
constructor.addParameter(VIEW, "source");
} else {
// 否则添加 Context context 参数
constructor.addParameter(CONTEXT, "context");
}
if (hasUnqualifiedResourceBindings()) {
// Aapt can change IDs out from underneath us, just suppress since all will work at runtime.
constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "ResourceType")
.build());
}
// 如果有父类,那么会根据不同情况调用不同的 super 语句
if (parentBinding != null) {
if (parentBinding.constructorNeedsView()) {
constructor.addStatement("super(target, source)");
} else if (constructorNeedsView()) {
constructor.addStatement("super(target, source.getContext())");
} else {
constructor.addStatement("super(target, context)");
}
constructor.addCode("\n");
}
// 如果有绑定 Field 或者方法,那么添加 this.target = target 语句
if (hasTargetField()) {
constructor.addStatement("this.target = target");
constructor.addCode("\n");
}
// 如果有 View 绑定
if (hasViewBindings()) {
if (hasViewLocal()) {
// Local variable in which all views will be temporarily stored.
constructor.addStatement("$T view", VIEW);
}
for (ViewBinding binding : viewBindings) {
// 为 View 绑定生成类似于 findViewById 之类的代码
addViewBinding(constructor, binding);
}
// 为 View 的集合或者数组绑定
for (FieldCollectionViewBinding binding : collectionBindings) {
constructor.addStatement("$L", binding.render());
}
if (!resourceBindings.isEmpty()) {
constructor.addCode("\n");
}
}
// 绑定 resource 资源的代码
if (!resourceBindings.isEmpty()) {
if (constructorNeedsView()) {
constructor.addStatement("$T context = source.getContext()", CONTEXT);
}
if (hasResourceBindingsNeedingResource(sdk)) {
constructor.addStatement("$T res = context.getResources()", RESOURCES);
}
for (ResourceBinding binding : resourceBindings) {
constructor.addStatement("$L", binding.render(sdk));
}
}
return constructor.build();
}

通过上面的代码就生成了构造器, findViewById 都在 addViewBinding(constructor, binding) 里会看到:

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 void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
if (binding.isSingleFieldBinding()) {
// Optimize the common case where there's a single binding directly to a field.
FieldViewBinding fieldBinding = binding.getFieldBinding();
// 注意这里直接使用了 target. 的形式,所以属性肯定是不能 private 的
CodeBlock.Builder builder = CodeBlock.builder()
.add("target.$L = ", fieldBinding.getName());
// 下面都是 View 绑定的代码
boolean requiresCast = requiresCast(fieldBinding.getType());
if (!requiresCast && !fieldBinding.isRequired()) {
builder.add("source.findViewById($L)", binding.getId().code);
} else {
builder.add("$T.find", UTILS);
builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
if (requiresCast) {
builder.add("AsType");
}
builder.add("(source, $L", binding.getId().code);
if (fieldBinding.isRequired() || requiresCast) {
builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
}
if (requiresCast) {
builder.add(", $T.class", fieldBinding.getRawType());
}
builder.add(")");
}
result.addStatement("$L", builder.build());
return;
}
List<MemberViewBinding> requiredBindings = binding.getRequiredBindings();
if (requiredBindings.isEmpty()) {
result.addStatement("view = source.findViewById($L)", binding.getId().code);
} else if (!binding.isBoundToRoot()) {
result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,
binding.getId().code, asHumanDescription(requiredBindings));
}
addFieldBinding(result, binding);
// OnClick 等监听事件绑定
addMethodBindings(result, binding);
}

至此,整个 ButterKnifeProcessor 解析注解、生成 Java 代码的流程就走完了。

结语

借鉴了很多大神的文章,非常感谢,也推荐给大家。

ButterKnife源码分析

Java注解处理器

Android中使用AbstractProcessor在编译时生成代码

0%