JakeWharton 是 Android 大神,同时也是开源狂魔。他开源的项目特色是小而美,且应用普遍,好比 butterknife、RxBinding、hugo 等,本文从受众最普遍,star 最多的 ButterKnife 讲起。javascript
经过阅读 ButterKnife 源码和本文,你将收获:html
ButterKnife 使用注解的方式来替代繁琐的 findViewById
和注册监听器时大量的匿名内部类写法。java
本文正对 8.5.1 版本的源码进行分析,自从 8.2.0 起已经支持 library 工程。github 地址为:github.com/JakeWharton… 。android
阅读源码切忌只见树木不见森林,所以先从大局上分析下这个项目。git
ButterKnife 共7个组件,他们的依赖关系以下图所示(其中,butterknife-integration-test 工程不作介绍):github
0.sample:表明使用 ButterKnife 的业务项目,根据上图所示须要依赖与3个组件,所以咱们在使用 ButterKnife 时须要作以下配置:api
dependencies {
compile 'com.jakewharton:butterknife:8.5.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
}复制代码
若是项目是 library ,还将引入第三个依赖android-studio
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:8.5.1'
}复制代码
为何须要这三个依赖,他们的做用分别是什么,下文将一一介绍。缓存
ButterKnife.bind(this)
,这是 ButterKnife 对外提供的门面。也是运行时,触发 Activity
中 View
控件绑定的时机。Activity
中 View
绑定的 Java 文件。### 总体流程框架
将整个流程拆分红编译期间和运行期间,就不难理解 ButterKnife 的运行机制。伴随而来的几个问题:
Activity
中 的View
控件?首先来解决第一个问题,编译期间和注解处理,经过这两个关键词,咱们能够联想到的技术方案是: APT(Annotation Processing Tool),即注解处理工具。在该方案中,一般有个必备的三件套,分别是注解处理器 Processor,注册注解处理器 AutoService 和代码生成工具 JavaPoet。
ButterKnife 一切皆注解,所以首先须要个处理器来解析注解。 ButterKnifeProcessor
充当了该角色,其中 process
方法是触发注解解析的入口,全部的神奇的事情从这里发生。
process
方法中主要作两件事情,分别是:
findAndParseTargets(env)
中解析注解的代码很是冗长,依次对 @BindArray
、@BindColor
、@BindString
、@BindView
等注解进行解析,解析结果存放在 bindingMap
中。
这里重点关注下 bindingMap
的键值对。key 值为 TypeElement
对象 ,能够简单的理解为被解析的类自己,而 value 值为 BindingSet
对象,该对象存放了解析结果,根据该结果,JavaPoet 将生成不一样的 Java 文件,以官方 sample 为例,其映射关系以下:
key | value | JavaPoet 根据 value 生成的文件 |
---|---|---|
SimpleActivity | BindingSet | SimpleActivity_ViewBinding.java |
SimpleAdapter | BindingSet | SimpleAdapter$ViewHolder_ViewBinding.java |
Processor 是为三件套之一。
在介绍余下二件套以前,先插播个小插曲,关于单元测试。
在阅读源码过程当中,debug 断点工具每每能够帮助咱们事半功倍,运行时的 debug 比较好处理,可是相似于 ButterKnife 这种须要在编译期间处理逻辑的代码应该如何进行 debug ?
单元测试能够把代码独立成一个单元,而且能够隔离对上下文、对环境的依赖(好比 Robolectric
对 Android 的 mock)。一个优秀的有态度的开源框架,每每都配备了齐全的单元测试,ButterKnife 也不例外。
butterknife 子组件中配备了大量的单元测试,这些单元测试是为 ButterKnifeProcessor
量身打造的。好比 ExtendActivityTest
中的 views()
对 Activity 包含@BindView
的注解时的处理作了单元测试,运行 UT 后,能够随意断点,以下图:
建议读者用这种方式来理解 butterknife-compiler 中的源码。
定义完注解处理器后,还须要告诉编译器该注解处理器的信息,需在 src/main/resource/META-INF/service
目录下增长 javax.annotation.processing.Processor
文件,并将注解处理器的类名配置在该文件中。
整个过程比较繁琐,Google 为咱们提供了更便利的工具,叫 AutoService,此时只须要为注解处理器增长 @AutoService
注解就能够了,以下:
@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
}复制代码
AutoService 是为 android-apt 三件套之二。
最后介绍下三件套中最诗情画意的一个工具—— JavaPoet。她提供了笔墨纸砚,让咱们像写诗同样写一个 Java 类。
了解 JavaPoet ,最好的方式即是看官方文档。简而言之,当咱们写一个类时,实际上是有固定结构的,JavaPoet 提供了生成这些结构的 api,举例以下:
TypeSpec.classBuilder()
MethodSpec.constructorBuilder()
MethodSpec.methodBuilder()
ParameterSpec.builder()
FieldSpec.builder()
CodeBlock.builder()
JavaPoet 提供了不少 Builder
,这即是咱们手中的笔墨纸砚。
有了浪漫的 Java 诗人以后,能够作不少充满想象力的事情。以 ButterKnife 而言,他作的事情即是将注解处理器解析后的结果(实际上就是上文提到的 BindingSet
对象)生成 Activity_ViewBinding.java
,该对象负责绑定 Activity
中的 View
控件以及设置监听器等。
举例以下,假设有以下 ActivIty
,
package com.geniusmart;
// 省略 import 语句
public class TestActivity extends Activity {
@BindView(1) View one; // 1 其实是Android resource对应的id
}复制代码
通过 JavaPoet 处理后,将生成以下文件:
package butterknife.compiler;
// 省略 import 语句
public class TestActivity_ViewBinding implements Unbinder {
private TestActivity target;
@UiThread
public TestActivity_ViewBinding(TestActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public TestActivity_ViewBinding(TestActivity target, View source) {
this.target = target;
target.one = Utils.findRequiredView(source, 1, "field 'one'");
}
@Override
@CallSuper
public void unbind() {
TestActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.one = null;
}
}复制代码
那么 JavaPoet 是如何处理的?实际上 ButterKnife 会将上文提到的 BindingSet
转换成相似于下文所示的代码:
// 建立类
TypeSpec typeSpec = TypeSpec.classBuilder("TestActivity_ViewBinding")
.addModifiers(PUBLIC) // 类为public
.addSuperinterface(UNBINDER) // 类为Unbinder的实现类
.addField(targetField) // 生成属性 private TestActivity target
.addMethod(constructorForActivity) // 生成构造器1
.addMethod(otherConstructor) // 生成构造器2
.addMethod(unBindeMethod) // 生成unbind()方法
.build();
// 生成 Java 文件
JavaFile javaFile = JavaFile.builder("com.geniusmart", typeSpec)//包名和类
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
javaFile.writeTo(System.out);复制代码
如需完整代码,请点击 PoetAboutButterKnife.java ,这是个单元测试,可直接运行,运行后能够在控制台看到生成的 Java 类。
最后总结下这三件套的协做流程,以下图:
接下来咱们来分析下运行期间发生的事情,相比于编译期间,运行期间的逻辑简单了许多。
public class SimpleActivity extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
}
}复制代码
运行时的入口在于 ButterKnife.bind(this)
,追溯源码发现,最终将会执行如下逻辑:
// 最终将找到 SimpleActivity_ViewBinding 的构造器,并实例化
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
constructor.newInstance(target, source);复制代码
也就是说 ButterKnife.bind(this)
等价于以下代码:
View sourceView = activity.getWindow().getDecorView();
new SimpleActivity_ViewBinding(activity,sourceView);复制代码
SimpleActivity_ViewBinding
持有Activity对象,而且在其构造器中,将会触发Activity 中 view 控件的绑定。
注:虽然这里使用了反射,但源码中将 Class.forName
的结果缓存起来后再经过 newInstance
建立实例,避免重复加载类,提高性能。
编译期间和运行期间相辅相成,这即是 android-apt 的广泛套路。
编译时和运行时的问题解决了,还有最后一个问题:由 R 生成 R2 的意义是什么?
若是你细心的话会发如今官方的 sample-library 中,注解的值均是由 R2 来引用的,以下图:
若是非 library 工程,则仍然引用系统生成的 R 文件。因此能够猜想:R2 的诞生是为 library 工程量身打造的。
其实 ButterKinife 在 8.2.0 版本以前,并不支持 library 工程的使用。在 Android 组件化、模块化需求这么迫切的今天,若是不支持 library 工程实在惋惜。JakeWharton 在2016年07月10日解决了此问题。
首先分析下为何 library 工程不直接引用 R?当咱们把 R2 改为 R 以后,编译器将会报错:Attribute value must be constant
,以下图:
也就是说 BindView
注解的属性必须是常量。可是在 library 工程中 R.id.title
的值为变量,以下图(注:并无 final
修饰符):
如何解决此问题?既然 R 不能知足要求,那就本身构建一个 R2,由 R 复制而来,而且将其属性都修改成 public static final
来修饰的常量。为了让使用者对整个过程无感知,所以使用 gradle 插件来解决这个需求,这也是 butterknife-gradle-plugin 工程的由来。
butterknife-gradle-plugin 有两个重要的第三方依赖,分别是 javaparser
和 javapoet
,前者用于解析 Java 文件,也就是解析 R 文件,后者在前文中已经浓彩重墨,用于将解析结果生成 R2 文件。
整个插件工程的源码并不难理解,在生成 R2 文件时,要将属性定义成 public static final
,在源码中咱们能够看到此逻辑,在 FinalRClassBuilder.addResourceField()
中 :
FieldSpec.Builder fieldSpecBuilder = FieldSpec.builder(int.class, fieldName)
.addModifiers(PUBLIC, STATIC, FINAL)
.initializer(fieldValue);复制代码
butterknife 插件在 processResources
的 Task 中执行,该任务一般用来完成文件的 copy。有关插件的知识笔者将在接下来的另一篇关于 hugo
的源码解析中介绍。
生成了 R2 文件后,会产生一个问题:该文件仅是为注解而用的,对开发者并无任何约束行为,怎么防止开发者误用?如:
int id = R2.id.footer;复制代码
若是写代码是应付工做,若是工做是绩效驱动,这类问题彻底不须要考虑。可是,做为优秀的、有态度的、有情怀的开源框架,JakeWharton 和 ButterKnife 给了咱们榜样,为了解决这个问题,butterknife-lint 工程应运而生。
从工程名来看,不难理解这工程的意义:一个静态代码检查工具,用来验证非法的 R2 引用。一旦在咱们的业务项目里不当心引用了 R2 文件,当执行 Lint 后,将会有以下图的提示信息:
追求完美的 JakeWharton ,有态度的 ButterKnife !
轮子每天有,可是好轮子并不常见。轮子的创意、价值、技术选项、单元测试以及追求完美的态度是衡量一个优秀轮子的维度。ButterKnife 完美地诠释了这一切。
blog.stablekernel.com/the-10-step…
github.com/google/auto…
github.com/square/java…