
Jake Wharton 是 Android 大神,同時(shí)也是開源狂魔。他開源的項(xiàng)目特點(diǎn)是小而美,且應(yīng)用廣泛,比如 butterknife、RxBinding、hugo 等,本文從受眾最廣泛,star 最多的 ButterKnife 講起。
(一) 你將獲得什么
通過閱讀 ButterKnife 源碼和本文,你將收獲:
- android-apt 三件套:
- 注解處理器(AbstractProcess)
- 注解處理器注冊(cè)(AutoService)
- 代碼生成(JavaPoet)
- 自定義 gradle 插件
- 造一個(gè)優(yōu)秀輪子應(yīng)該具備的態(tài)度
(二)ButterKnife 簡介
ButterKnife 使用注解的方式來替代繁瑣的 findViewById 和注冊(cè)監(jiān)聽器時(shí)大量的匿名內(nèi)部類寫法。
本文針對(duì) 8.5.1 版本的源碼進(jìn)行分析,自從 8.2.0 起已經(jīng)支持 library 工程。github 地址為:https://github.com/JakeWharton/butterknife/releases/tag/8.5.1 。
(三)ButterKnife 總覽
閱讀源碼切忌只見樹木不見森林,因此先從大局上分析下這個(gè)項(xiàng)目。
組件依賴關(guān)系
ButterKnife 共7個(gè)組件,他們的依賴關(guān)系如下圖所示(其中,butterknife-integration-test 工程不做介紹):

- 0.sample:代表使用 ButterKnife 的業(yè)務(wù)項(xiàng)目,根據(jù)上圖所示需要依賴與3個(gè)組件,因此我們?cè)谑褂?ButterKnife 時(shí)需要做如下配置:
dependencies {
compile 'com.jakewharton:butterknife:8.5.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
}
如果項(xiàng)目是 library ,還將引入第三個(gè)依賴
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:8.5.1'
}
為什么需要這三個(gè)依賴,他們的作用分別是什么,下文將一一介紹。
- 1.butterknife:這個(gè)工程提供了
ButterKnife.bind(this),這是 ButterKnife 對(duì)外提供的門面。也是運(yùn)行時(shí),觸發(fā)Activity中View控件綁定的時(shí)機(jī)。 - 2.butterknife-compiler:見名知意,編譯期間將使用該工程,他的作用是解析注解,并且生成
Activity中View綁定的 Java 文件。 - 3.butterknife-annotations:將所有自定義的注解放在此工程下, 確保職責(zé)的單一。
- 4.butterknife-gradle-plugin:gradle 插件,這是8.2.0版本起為了支持 library 工程而新增的一個(gè)插件工程,原理將在下文中詳細(xì)介紹。
- 5.butterknife-lint:針對(duì) butterknife-gradle-plugin 而做的靜態(tài)代碼檢查工具,非常有態(tài)度的一種做法,在下文做詳細(xì)介紹。
整體流程

將整個(gè)流程拆分成編譯期間和運(yùn)行期間,就不難理解 ButterKnife 的運(yùn)行機(jī)制。伴隨而來的幾個(gè)問題:
- 編譯期間如何處理注解的信息,并解析生成 Java 文件?
- 運(yùn)行期間如何綁定
Activity中 的View控件? - 由 R 生成 R2 的意義是什么?
(四)android-apt(Annotation Processing Tool)
首先來解決第一個(gè)問題,編譯期間和注解處理,通過這兩個(gè)關(guān)鍵詞,我們可以聯(lián)想到的技術(shù)方案是: APT(Annotation Processing Tool),即注解處理工具。在該方案中,通常有個(gè)必備的三件套,分別是注解處理器 Processor,注冊(cè)注解處理器 AutoService 和代碼生成工具 JavaPoet。
三件套之注解處理器
ButterKnife 一切皆注解,因此首先需要個(gè)處理器來解析注解。 ButterKnifeProcessor 充當(dāng)了該角色,其中 process 方法是觸發(fā)注解解析的入口,所有的神奇的事情從這里發(fā)生。
process 方法中主要做兩件事情,分別是:
- 解析所有包含了 ButterKnife 注解的類
- 根據(jù)解析結(jié)果,使用 JavaPoet 生成相應(yīng)的Java文件

findAndParseTargets(env) 中解析注解的代碼非常冗長,依次對(duì) @BindArray 、@BindColor、@BindString、@BindView 等注解進(jìn)行解析,解析結(jié)果存放在 bindingMap 中。
這里重點(diǎn)關(guān)注下 bindingMap 的鍵值對(duì)。key 值為 TypeElement 對(duì)象 ,可以簡單的理解為被解析的類本身,而 value 值為 BindingSet 對(duì)象,該對(duì)象存放了解析結(jié)果,根據(jù)該結(jié)果,JavaPoet 將生成不同的 Java 文件,以官方 sample 為例,其映射關(guān)系如下:
| key | value | JavaPoet 根據(jù) value 生成的文件 |
|---|---|---|
| SimpleActivity | BindingSet | SimpleActivity_ViewBinding.java |
| SimpleAdapter | BindingSet | SimpleAdapter$ViewHolder_ViewBinding.java |

Processor 是為三件套之一。
小插曲之 UT
在介紹余下二件套之前,先插播個(gè)小插曲,關(guān)于單元測(cè)試。
在閱讀源碼過程中,debug 斷點(diǎn)工具往往可以幫助我們事半功倍,運(yùn)行時(shí)的 debug 比較好處理,但是類似于 ButterKnife 這種需要在編譯期間處理邏輯的代碼應(yīng)該如何進(jìn)行 debug ?
單元測(cè)試可以把代碼獨(dú)立成一個(gè)單元,并且可以隔離對(duì)上下文、對(duì)環(huán)境的依賴(比如 Robolectric 對(duì) Android 的 mock)。一個(gè)優(yōu)秀的有態(tài)度的開源框架,往往都配備了齊全的單元測(cè)試,ButterKnife 也不例外。
butterknife 子組件中配備了大量的單元測(cè)試,這些單元測(cè)試是為 ButterKnifeProcessor 量身打造的。比如 ExtendActivityTest 中的 views() 對(duì) Activity 包含@BindView 的注解時(shí)的處理做了單元測(cè)試,運(yùn)行 UT 后,可以隨意斷點(diǎn),如下圖:

建議讀者用這種方式來理解 butterknife-compiler 中的源碼。
三件套之注冊(cè)注解處理器
定義完注解處理器后,還需要告訴編譯器該注解處理器的信息,需在 src/main/resource/META-INF/service 目錄下增加 javax.annotation.processing.Processor 文件,并將注解處理器的類名配置在該文件中。
整個(gè)過程比較繁瑣,Google 為我們提供了更便利的工具,叫 AutoService,此時(shí)只需要為注解處理器增加 @AutoService 注解就可以了,如下:
@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
}
AutoService 是為 android-apt 三件套之二。
三件套之 Java 詩人
最后介紹下三件套中最詩情畫意的一個(gè)工具—— JavaPoet。她提供了筆墨紙硯,讓我們像寫詩一樣寫一個(gè) Java 類。
了解 JavaPoet ,最好的方式便是看官方文檔。簡而言之,當(dāng)我們寫一個(gè)類時(shí),其實(shí)是有固定結(jié)構(gòu)的,JavaPoet 提供了生成這些結(jié)構(gòu)的 api,舉例如下:
- 類:
TypeSpec.classBuilder() - 構(gòu)造器:
MethodSpec.constructorBuilder() - 方法:
MethodSpec.methodBuilder() - 參數(shù):
ParameterSpec.builder() - 屬性:
FieldSpec.builder() - 程序片段:
CodeBlock.builder()
JavaPoet 提供了很多 Builder,這便是我們手中的筆墨紙硯。
有了浪漫的 Java 詩人之后,可以做很多充滿想象力的事情。以 ButterKnife 而言,他做的事情便是將注解處理器解析后的結(jié)果(實(shí)際上就是上文提到的 BindingSet 對(duì)象)生成 Activity_ViewBinding.java,該對(duì)象負(fù)責(zé)綁定 Activity 中的 View 控件以及設(shè)置監(jiān)聽器等。
舉例如下,假設(shè)有如下 ActivIty,
package com.geniusmart;
// 省略 import 語句
public class TestActivity extends Activity {
@BindView(1) View one; // 1 實(shí)際上是Android resource對(duì)應(yīng)的id
}
經(jīng)過 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 是如何處理的?實(shí)際上 ButterKnife 會(huì)將上文提到的 BindingSet 轉(zhuǎn)換成類似于下文所示的代碼:
// 創(chuàng)建類
TypeSpec typeSpec = TypeSpec.classBuilder("TestActivity_ViewBinding")
.addModifiers(PUBLIC) // 類為public
.addSuperinterface(UNBINDER) // 類為Unbinder的實(shí)現(xiàn)類
.addField(targetField) // 生成屬性 private TestActivity target
.addMethod(constructorForActivity) // 生成構(gòu)造器1
.addMethod(otherConstructor) // 生成構(gòu)造器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);
如需完整代碼,請(qǐng)點(diǎn)擊 PoetAboutButterKnife.java ,這是個(gè)單元測(cè)試,可直接運(yùn)行,運(yùn)行后可以在控制臺(tái)看到生成的 Java 類。
最后總結(jié)下這三件套的協(xié)作流程,如下圖:

(五)運(yùn)行期間
接下來我們來分析下運(yùn)行期間發(fā)生的事情,相比于編譯期間,運(yùn)行期間的邏輯簡單了許多。
public class SimpleActivity extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
}
}
運(yùn)行時(shí)的入口在于 ButterKnife.bind(this),追溯源碼發(fā)現(xiàn),最終將會(huì)執(zhí)行以下邏輯:
// 最終將找到 SimpleActivity_ViewBinding 的構(gòu)造器,并實(shí)例化
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
constructor.newInstance(target, source);
也就是說 ButterKnife.bind(this) 等價(jià)于如下代碼:
View sourceView = activity.getWindow().getDecorView();
new SimpleActivity_ViewBinding(activity,sourceView);
SimpleActivity_ViewBinding 持有Activity對(duì)象,并且在其構(gòu)造器中,將會(huì)觸發(fā)Activity 中 view 控件的綁定。
注:雖然這里使用了反射,但源碼中將 Class.forName 的結(jié)果緩存起來后再通過 newInstance 創(chuàng)建實(shí)例,避免重復(fù)加載類,提升性能。
編譯期間和運(yùn)行期間相輔相成,這便是 android-apt 的普遍套路。
(六)支持 library
編譯時(shí)和運(yùn)行時(shí)的問題解決了,還有最后一個(gè)問題:由 R 生成 R2 的意義是什么?
如果你細(xì)心的話會(huì)發(fā)現(xiàn)在官方的 sample-library 中,注解的值均是由 R2 來引用的,如下圖:

如果非 library 工程,則仍然引用系統(tǒng)生成的 R 文件。所以可以猜測(cè):R2 的誕生是為 library 工程量身打造的。
其實(shí) ButterKinife 在 8.2.0 版本之前,并不支持 library 工程的使用。在 Android 組件化、模塊化需求這么迫切的今天,如果不支持 library 工程實(shí)在可惜。JakeWharton 在2016年07月10日解決了此問題。
首先分析下為什么 library 工程不直接引用 R?當(dāng)我們把 R2 改成 R 之后,編譯器將會(huì)報(bào)錯(cuò):Attribute value must be constant ,如下圖:

也就是說 BindView 注解的屬性必須是常量。但是在 library 工程中 R.id.title 的值為變量,如下圖(注:并沒有 final 修飾符):

如何解決此問題?既然 R 不能滿足要求,那就自己構(gòu)建一個(gè) R2,由 R 復(fù)制而來,并且將其屬性都修改為 public static final 來修飾的常量。為了讓使用者對(duì)整個(gè)過程無感知,因此使用 gradle 插件來解決這個(gè)需求,這也是 butterknife-gradle-plugin 工程的由來。
butterknife-gradle-plugin 有兩個(gè)重要的第三方依賴,分別是 javaparser 和 javapoet ,前者用于解析 Java 文件,也就是解析 R 文件,后者在前文中已經(jīng)濃彩重墨,用于將解析結(jié)果生成 R2 文件。
整個(gè)插件工程的源碼并不難理解,在生成 R2 文件時(shí),要將屬性定義成 public static final ,在源碼中我們可以看到此邏輯,在 FinalRClassBuilder.addResourceField() 中 :
FieldSpec.Builder fieldSpecBuilder = FieldSpec.builder(int.class, fieldName)
.addModifiers(PUBLIC, STATIC, FINAL)
.initializer(fieldValue);
butterknife 插件在 processResources 的 Task 中執(zhí)行,該任務(wù)通常用來完成文件的 copy。有關(guān)插件的知識(shí)筆者將在接下來的另外一篇關(guān)于 hugo 的源碼解析中介紹。
(七)有態(tài)度的 Lint 檢查
生成了 R2 文件后,會(huì)產(chǎn)生一個(gè)問題:該文件僅是為注解而用的,對(duì)開發(fā)者并沒有任何約束力,怎么防止開發(fā)者誤用?如:
int id = R2.id.footer;
如果寫代碼是應(yīng)付工作,如果工作是績效驅(qū)動(dòng),這類問題完全不需要考慮。但是,作為優(yōu)秀的、有態(tài)度的、有情懷的開源框架,JakeWharton 和 ButterKnife 給了我們榜樣,為了解決這個(gè)問題,butterknife-lint 工程應(yīng)運(yùn)而生。
從工程名來看,不難理解這工程的意義:一個(gè)靜態(tài)代碼檢查工具,用來驗(yàn)證非法的 R2 引用。一旦在我們的業(yè)務(wù)項(xiàng)目里不小心引用了 R2 文件,當(dāng)執(zhí)行 Lint 后,將會(huì)有如下圖的提示信息:

追求完美的 JakeWharton ,有態(tài)度的 ButterKnife !
(八)總結(jié)
輪子天天有,但是好輪子并不常見。輪子的創(chuàng)意、價(jià)值、技術(shù)選型、單元測(cè)試以及追求完美的態(tài)度是衡量一個(gè)優(yōu)秀輪子的維度。ButterKnife 完美地詮釋了這一切。
參考文章
http://blog.stablekernel.com/the-10-step-guide-to-annotation-processing-in-android-studio
https://github.com/google/auto/tree/master/service
https://github.com/square/javapoet