手撕黄油刀--探究ButterKnife的实现原理

上篇文章《Java编译时注解处理器(APT)详解》中学习了Java APT技术在Android中的使用,并且我们知道,当前Android开发中常用的许多框架都使用了APT技术,并且ButterKnife就是利用APT来实现的。那么本篇内容我们就来探究一下ButterKnife的实现原理。

一、ButterKnife的bind过程

当然,在探究源码,还是首先应该了解如何使用,我们以ButterKnife的BindView为例。首先在Activity的onCreate方法中通过ButterKnife.bind()方法绑定,然后为TextView添加BindView注解即可拿到TextView的实例。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.zhpan.app;

public class MainActivity extends AppCompatActivity {
@BindView(R.id.text_view)
TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}

ButterKnife是怎么做到通过注解获取TextView的实例的呢?我们就从bind方法开始吧,点击ButterKnife的bind方法,其代码如下:

1
2
3
4
5
//	ButterKnife中有很多bind的重载方法,仅以此为例
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}

可以看到在bind方法中通过Activity拿到了DecorView(至于不懂DecorView是什么的同学可以自行Google)。然后调用了createBinding方法,并传入Activity对象和DecorView两个参数。我们追踪createBinding的代码如下:

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
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
// 获取到了与target相关的类的构造方法
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

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

//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
// 通过构造方法反射实例化了这个类,这个类接收两个参数,分别是上边的Activity和DecorView
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create binding instance.", cause);
}
}

上述代码中通过findBindingConstructorForClass()方法得到了某个类的构造方法,并在接下来的代码中通过反射 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
22
23
24
25
26
27
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
// 从BINDINGS中获取cls对应的Unbinder
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) { 如果已存在直接return
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
// 这里拿到的clsName应该为“com.zhpan.app.MainActivity”
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 {
// 从类加载器中获取“com.zhpan.app.MainActivity_ViewBinding”
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
// 获取“com.zhpan.app.MainActivity_ViewBinding”类中的构造方法
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} 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;
}

直接看第7行,通过 cls.getName()得到了bind方法中参数的名字,由于我们是再MainActivity中调用的bind方法,因此,这里拿到的名字就是“com.zhpan.app.MainActivity”,接下来再第13行中通过ClassLoader拿到了一个名字是”clsName”+”_ViewBinding”的类,也就是名字为MainActivity_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
package com.zhpan.app;

public 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.mTextView = Utils.findRequiredViewAsType(source, R.id.text_view, "field 'mTextView'", TextView.class);
}

@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleare![在这里插入图片描述](https://img-blog.csdnimg.cn/20190825162306549.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70)d.");
this.target = null;

target.mTextView = null;
}
}

在这个类的构造方法中我们可以看到

1
target.mTextView = Utils.findRequiredViewAsType(source, R.id.text_view, "field 'mTextView'", TextView.class);

通过这句代码给Activity中的mTextView赋了值,到这里也就不奇怪为什通过一个BindView的注解就得到了mTextView的对象了。而MainActivity_ViewBinding这个类是从哪里来的呢?想必看过上篇文章同学应该都知道了,就是通过APT在代码编译期间自动生成的。其实后面的代码其实已经没有必要去看了,无非就是通过AbstractProcessor来处理注解,然后根据注解自动生成所需代码的。但是写文章要有始有终,也本着负责任的态度,还是深入ButtereKnife内部一探究竟。

二、探究ButterKnife的注解处理器ButterKnifeProcessor

现在我们来看一下ButterKnife的代码的模块结构:

在这里插入图片描述
看到butterknife-annotations和butterknife-compiler很眼熟?和上篇文章我们自己写的代码结构是一样的,butterknife-annotations模块下存放的是butterknife的所有注解,butterknife-compiler是用来处理butterknife-annotations注解的。所以们直接看butterknife-compiler模块下的ButterKnifeProcessor类。

1.ButterKnifeProcessor的getSupportedAnnotationTypes()方法

我们知道ButterKnife除了BindView注解之外还有许多其它注解,比如常用的BindColor注解、OnClick注解等。这些注解的名字都会被添加到getSupportedAnnotationTypes方法中的Set集合里。来看getSupportedAnnotationTypes的源码:

1
2
3
4
5
6
7
8
@Override public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
// 遍历ButterKnife中的所有注解,并将注解名字添加到Set集合
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}

2.ButterKnifeProcessor的process方法

我们知道process方法是整个注解处理其的核心方法,对注解的处理以及代码的生成都是在这个方法里边实现的。那么接下来,就一步步看ButterKnife是怎么处理注解和生成代码的。
**(1)解析RoundEnviroment**
在process方法的第一行我们看到通过findAndParseTargets方法得到了一个 Map<TypeElement, BindingSet>集合,代码如下:

1
2
3
4
5
6
7
8
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// 通过findAndParseTargets处理注解得到BindingSet的集合
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

...

return false;
}

而在findAndParseTargets(env)是对一系列注解的处理,这里我们仅以处理BindView为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}

// 省略处理其它注解的代码
...


第9行在parseBindView方法中对BindView注解进行了一系列的校验,通过校验后解析BindView的数据,比如view的Id等信息封装到了BindingSet.Builder中,并最终将封装好的BindingSet.Builder放入builderMap 中。由于parseBindView方法的代码比较多,这里就不再贴出了,有兴趣的可以直接下载源码查看。
接下来得到builderMap的集合之后,由于findAndParseTargets方法返回值是Map<TypeElement, BindingSet>,因此接下来又对builderMap集合进行了遍历并最终存储到了 Map<TypeElement, BindingSet>中。代码如下:

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
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {

...

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();

TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();

TypeElement parentType = findParentType(type, erasedTargetNames);
if (parentType == null) {
bindingMap.put(type, builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}
}
}

好了,接下来,继续到process方法中,在拿到bindingMap 之后遍历bindingMap ,并调用BindingSet的brewJava来生成Java文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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, useAndroidX);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}

return false;
}

3.JavaPoet生成代码

BindingSet 的brewJava方法中同样是使用了JavaPoet来生成代码的,无非就是拼接类,构造方法、方法,这些感觉真的没什么说的了。

1
2
3
4
5
6
JavaFile brewJava(int sdk, boolean debuggable, boolean useAndroidX) {
TypeSpec bindingConfiguration = createType(sdk, debuggable, useAndroidX);
return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
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
private TypeSpec createType(int sdk, boolean debuggable, boolean useAndroidX) {
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
if (isFinal) {
result.addModifiers(FINAL);
}

if (parentBinding != null) {
result.superclass(parentBinding.bindingClassName);
} else {
result.addSuperinterface(UNBINDER);
}

if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}

if (isView) {
result.addMethod(createBindingConstructorForView(useAndroidX));
} else if (isActivity) {
result.addMethod(createBindingConstructorForActivity(useAndroidX));
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog(useAndroidX));
}
if (!constructorNeedsView()) {
// Add a delegating constructor with a target type + view signature for reflective use.
result.addMethod(createBindingViewDelegateConstructor(useAndroidX));
}
result.addMethod(createBindingConstructor(sdk, debuggable, useAndroidX));

if (hasViewBindings() || parentBinding == null) {
result.addMethod(createBindingUnbindMethod(result, useAndroidX));
}

return result.build();
}

关于ButterKnife的源码就说这么多吧。总结一下,其实就是根据注解使用APT及JavaPoet在项目编译期间自动生成代码,并达到为注解元素赋值或者添加监听的目的。在阅读Butterknife时候不一定要把每一句代码都搞懂,能做到掌握其核心原理便可。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!