JakeWharton 是 Android 大神,同時也是開源狂魔。他開源的項目特色是小而美,且應用普遍,好比 butterknife、RxBinding、hugo 等,本文從受衆最普遍,star 最多的 ButterKnife 講起。html
經過閱讀 ButterKnife 源碼和本文,你將收穫:java
ButterKnife 使用註解的方式來替代繁瑣的 findViewById
和註冊監聽器時大量的匿名內部類寫法。android
本文正對 8.5.1 版本的源碼進行分析,自從 8.2.0 起已經支持 library 工程。github 地址爲:github.com/JakeWharton… 。git
閱讀源碼切忌只見樹木不見森林,所以先從大局上分析下這個項目。github
ButterKnife 共7個組件,他們的依賴關係以下圖所示(其中,butterknife-integration-test 工程不作介紹):api
0.sample:表明使用 ButterKnife 的業務項目,根據上圖所示須要依賴與3個組件,所以咱們在使用 ButterKnife 時須要作以下配置:android-studio
dependencies { compile 'com.jakewharton:butterknife:8.5.1' annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1' }複製代碼
若是項目是 library ,還將引入第三個依賴緩存
dependencies { classpath 'com.jakewharton:butterknife-gradle-plugin:8.5.1' }複製代碼
爲何須要這三個依賴,他們的做用分別是什麼,下文將一一介紹。markdown
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…