一、前言

Butterknife 依赖注入框架,简化手动书写 findViewById 等等,这里我们将深入源码来看看 Butterknife 内部的实现原理。

二、基本用法

首先,再复习一下基本的用法。

1
2
3
4
5
6
7
8
9
10
11
class MainActivity : AppCompatActivity() {

@BindView(R.id.btn_jetpack) //2
lateinit var btnJetPack: Button

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ButterKnife.bind(this) //1
}
}

可以看到,用 @BindView 注解替代了 findViewById,当然还有其他的用法,比如 @OnClick 注解替代 setOnClickListener() 等等,但其内部原理基本一致,所以这里我们仅对 @BindView 的内部实现来解析 Butterknife 的实现。

看到这里,你可能会有疑问,注解是什么?为什么通过一个简单的注解就能替代简化代码呢?

在进入源码之前,我们得先了解一下注解。

首先,得先知道 APT(Annotation Processing Tool) 注解处理工具,网上其实有很多教程之类的,我们这里便不再深入,简单的了解一下 APT 的实现步骤以及其作用时机。

  • 先声明一个注解 @interface,定义注解的生命周期 @Retention 和作用区域 @Target
  • 然后定义一个注解处理器,继承并实现 AbstractProcessor 中的方法;
  • 最后,代码在编译时,编译器会扫描所有带注解的类,通过定义的注解处理器来生成相应的模板 Java 类。

下面,我们将进入 Butterknife 的源码世界。

三、源码浅析

3.1 Butterknife.bind()

我们先来看看 ButterKnife.bind(this) 这里面到底做了什么处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//ButterKnife.java
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}

public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();

Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

if (constructor == null) {
return Unbinder.EMPTY;
}

//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);
}
//...
}

代码清晰简单,主要是找到目标类的构造器 constructor ,然后通过反射的方式实例化。

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
//ButterKnife.java
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null || BINDINGS.containsKey(cls)) {
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")
|| clsName.startsWith("androidx.")) {
return null;
}
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;
}

首先,使用了 LinkedHashMap 作为缓存,避免每次重新加载。然后过滤了系统相关类,如果是 android.java.androidx. 开头的不作处理。最后通过类加载器加载对应的 _ViewBinding 类,比如这边的例子中应该是 MainActivity_ViewBinding 类。

到这里,我们就会产生一些疑问,我们在例子里都没写过 MainActivity_ViewBinding 这个类,那它从哪里生成的呢?又是做什么的?

在 AS 中搜索一下,可以发现在 /app/build/generated/source/kapt/debug/包名/ 目录下找到了此类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;

@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}

@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;

target.btnJetPack = Utils.findRequiredViewAsType(source, R.id.btn_jetpack, "field 'btnJetPack'", Button.class);
}

@Override
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;

target.btnJetPack = null;
}
}

可以发现代码非常简单,在构造方法中寻找对应的控件。这边有一点稍微注意一下,我们在使用 Butterknife 的时候,控件的修饰符不能为 private 的原因就在这里,是直接赋值而不是通过 set 方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Utils.java
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}

public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(id);
if (view != null) {
return view;
}
//...
}

public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
try {
return cls.cast(view);
}
//...
}

最终也是通过 findViewById 来寻找控件。

到这里,我们可以知道生成的以 _ViewBinding 结尾的类主要是用来寻找被 @BindView 注解标识的控件。那么该类是在哪里生成的呢?

3.2 ButterKnifeProcessor

在前面,我们说过 APT 需要声明一个注解以及一个注解处理器,以便编译器在编译的时候作相应的处理。

1
2
3
4
5
@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}

声明了一个 BindView 的注解。

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
//ButterKnifeProcessor.java
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
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);
try {
javaFile.writeTo(filer);
}
//...
}
return false;
}

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

//...

// Process each @BindView element.
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);
}
}

//...

return bindingMap;
}

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

//...

// Assemble information on the field.
int id = element.getAnnotation(BindView.class).value();
BindingSet.Builder builder = builderMap.get(enclosingElement);
Id resourceId = elementToId(element, BindView.class, id);
if (builder != null) {
String existingBindingName = builder.findExistingBindingName(resourceId);
//...
} else {
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);
}

private BindingSet.Builder getOrCreateBindingBuilder(
Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder == null) {
builder = BindingSet.newBuilder(enclosingElement);
builderMap.put(enclosingElement, builder);
}
return builder;
}

ButterKnifeProcessor 的代码这里精简了很多,抓主流程就好,更多的细节如果有兴趣可以自己跟下源码。在 process() 方法中,主要是寻找被 BindView 注解标识的类存到集合中,最后循环取出通过 javapoet 生成 _ViewBinding 模板代码类。

四、总结

通过上面的简单的源码解析,我们大概清楚了 Butterknife 的实现原理。当然 Butterknife 并不仅仅这么简单,还有其他的功能,但原理是一样的。

最后,我们简单做个总结:

  1. 首先,在编译期间扫描声明的注解(如 BindView ),通过 ButterKnifeProcessor 注解处理器相应的解析以及调用 Javapoet 库生成 _ViewBinding 模板代码。
  2. 然后,在我们调用 ButterKnife.bind(this) 方法的时候,通过类加载器加载指定的类(如 MainActivity_ViewBinding ),并通过反射方式实例化该模板类,从而实现了对标识注解的控件赋值。

五、参考与推荐

  1. ButterKnife