拆 Jake Wharton 系列之 ButterKnife

Jake Wharton 是 Android 大神,同時也是開源狂魔。他開源的項目特點(diǎn)是小而美,且應(yīng)用廣泛,比如 butterknifeRxBinding、hugo 等,本文從受眾最廣泛,star 最多的 ButterKnife 講起。

(一) 你將獲得什么

通過閱讀 ButterKnife 源碼和本文,你將收獲:

  • android-apt 三件套:
  1. 注解處理器(AbstractProcess)
  2. 注解處理器注冊(AutoService)
  3. 代碼生成(JavaPoet)
  • 自定義 gradle 插件
  • 造一個優(yōu)秀輪子應(yīng)該具備的態(tài)度

(二)ButterKnife 簡介

ButterKnife 使用注解的方式來替代繁瑣的 findViewById 和注冊監(jiān)聽器時大量的匿名內(nèi)部類寫法。

本文針對 8.5.1 版本的源碼進(jìn)行分析,自從 8.2.0 起已經(jīng)支持 library 工程。github 地址為:https://github.com/JakeWharton/butterknife/releases/tag/8.5.1 。

(三)ButterKnife 總覽

閱讀源碼切忌只見樹木不見森林,因此先從大局上分析下這個項目。

組件依賴關(guān)系

ButterKnife 共7個組件,他們的依賴關(guān)系如下圖所示(其中,butterknife-integration-test 工程不做介紹):

butterknife組件依賴
  • 0.sample:代表使用 ButterKnife 的業(yè)務(wù)項目,根據(jù)上圖所示需要依賴與3個組件,因此我們在使用 ButterKnife 時需要做如下配置:
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'
 }

為什么需要這三個依賴,他們的作用分別是什么,下文將一一介紹。

  • 1.butterknife:這個工程提供了 ButterKnife.bind(this),這是 ButterKnife 對外提供的門面。也是運(yùn)行時,觸發(fā) ActivityView 控件綁定的時機(jī)。
  • 2.butterknife-compiler:見名知意,編譯期間將使用該工程,他的作用是解析注解,并且生成 ActivityView 綁定的 Java 文件。
  • 3.butterknife-annotations:將所有自定義的注解放在此工程下, 確保職責(zé)的單一。
  • 4.butterknife-gradle-plugin:gradle 插件,這是8.2.0版本起為了支持 library 工程而新增的一個插件工程,原理將在下文中詳細(xì)介紹。
  • 5.butterknife-lint:針對 butterknife-gradle-plugin 而做的靜態(tài)代碼檢查工具,非常有態(tài)度的一種做法,在下文做詳細(xì)介紹。

整體流程

butterknife-流程圖

將整個流程拆分成編譯期間和運(yùn)行期間,就不難理解 ButterKnife 的運(yùn)行機(jī)制。伴隨而來的幾個問題:

  1. 編譯期間如何處理注解的信息,并解析生成 Java 文件?
  2. 運(yùn)行期間如何綁定 Activity 中 的View 控件?
  3. 由 R 生成 R2 的意義是什么?

(四)android-apt(Annotation Processing Tool)

首先來解決第一個問題,編譯期間注解處理,通過這兩個關(guān)鍵詞,我們可以聯(lián)想到的技術(shù)方案是: APT(Annotation Processing Tool),即注解處理工具。在該方案中,通常有個必備的三件套,分別是注解處理器 Processor,注冊注解處理器 AutoService 和代碼生成工具 JavaPoet。

三件套之注解處理器

ButterKnife 一切皆注解,因此首先需要個處理器來解析注解。 ButterKnifeProcessor 充當(dāng)了該角色,其中 process 方法是觸發(fā)注解解析的入口,所有的神奇的事情從這里發(fā)生。

process 方法中主要做兩件事情,分別是:

  1. 解析所有包含了 ButterKnife 注解的類
  2. 根據(jù)解析結(jié)果,使用 JavaPoet 生成相應(yīng)的Java文件
process源碼

findAndParseTargets(env) 中解析注解的代碼非常冗長,依次對 @BindArray@BindColor、@BindString、@BindView 等注解進(jìn)行解析,解析結(jié)果存放在 bindingMap 中。

這里重點(diǎn)關(guān)注下 bindingMap 的鍵值對。key 值為 TypeElement 對象 ,可以簡單的理解為被解析的類本身,而 value 值為 BindingSet 對象,該對象存放了解析結(jié)果,根據(jù)該結(jié)果,JavaPoet 將生成不同的 Java 文件,以官方 sample 為例,其映射關(guān)系如下:

key value JavaPoet 根據(jù) value 生成的文件
SimpleActivity BindingSet SimpleActivity_ViewBinding.java
SimpleAdapter BindingSet SimpleAdapter$ViewHolder_ViewBinding.java
生成的 java 文件

Processor 是為三件套之一。

小插曲之 UT

在介紹余下二件套之前,先插播個小插曲,關(guān)于單元測試。

在閱讀源碼過程中,debug 斷點(diǎn)工具往往可以幫助我們事半功倍,運(yùn)行時的 debug 比較好處理,但是類似于 ButterKnife 這種需要在編譯期間處理邏輯的代碼應(yīng)該如何進(jìn)行 debug ?

單元測試可以把代碼獨(dú)立成一個單元,并且可以隔離對上下文、對環(huán)境的依賴(比如 Robolectric 對 Android 的 mock)。一個優(yōu)秀的有態(tài)度的開源框架,往往都配備了齊全的單元測試,ButterKnife 也不例外。

butterknife 子組件中配備了大量的單元測試,這些單元測試是為 ButterKnifeProcessor 量身打造的。比如 ExtendActivityTest 中的 views() 對 Activity 包含@BindView 的注解時的處理做了單元測試,運(yùn)行 UT 后,可以隨意斷點(diǎn),如下圖:

對ButterKnifeProcessor斷點(diǎn)調(diào)試

建議讀者用這種方式來理解 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 三件套之二。

三件套之 Java 詩人

最后介紹下三件套中最詩情畫意的一個工具—— JavaPoet。她提供了筆墨紙硯,讓我們像寫詩一樣寫一個 Java 類。

了解 JavaPoet ,最好的方式便是看官方文檔。簡而言之,當(dāng)我們寫一個類時,其實是有固定結(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é)果(實際上就是上文提到的 BindingSet 對象)生成 Activity_ViewBinding.java,該對象負(fù)責(zé)綁定 Activity 中的 View 控件以及設(shè)置監(jiān)聽器等。

舉例如下,假設(shè)有如下 ActivIty,

package com.geniusmart;
// 省略 import 語句

public class TestActivity extends Activity {
 @BindView(1) View one; // 1 實際上是Android resource對應(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 是如何處理的?實際上 ButterKnife 會將上文提到的 BindingSet 轉(zhuǎn)換成類似于下文所示的代碼:

// 創(chuàng)建類
TypeSpec typeSpec = TypeSpec.classBuilder("TestActivity_ViewBinding")
        .addModifiers(PUBLIC) // 類為public
        .addSuperinterface(UNBINDER) // 類為Unbinder的實現(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);

如需完整代碼,請點(diǎn)擊 PoetAboutButterKnife.java ,這是個單元測試,可直接運(yùn)行,運(yùn)行后可以在控制臺看到生成的 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)行時的入口在于 ButterKnife.bind(this),追溯源碼發(fā)現(xiàn),最終將會執(zhí)行以下邏輯:

// 最終將找到 SimpleActivity_ViewBinding 的構(gòu)造器,并實例化
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對象,并且在其構(gòu)造器中,將會觸發(fā)Activity 中 view 控件的綁定。

注:雖然這里使用了反射,但源碼中將 Class.forName 的結(jié)果緩存起來后再通過 newInstance 創(chuàng)建實例,避免重復(fù)加載類,提升性能。

編譯期間和運(yùn)行期間相輔相成,這便是 android-apt 的普遍套路。

(六)支持 library

編譯時和運(yùn)行時的問題解決了,還有最后一個問題:由 R 生成 R2 的意義是什么?

如果你細(xì)心的話會發(fā)現(xiàn)在官方的 sample-library 中,注解的值均是由 R2 來引用的,如下圖:


R2 的使用

如果非 library 工程,則仍然引用系統(tǒng)生成的 R 文件。所以可以猜測:R2 的誕生是為 library 工程量身打造的。

其實 ButterKinife 在 8.2.0 版本之前,并不支持 library 工程的使用。在 Android 組件化、模塊化需求這么迫切的今天,如果不支持 library 工程實在可惜。JakeWharton 在2016年07月10日解決了此問題。

首先分析下為什么 library 工程不直接引用 R?當(dāng)我們把 R2 改成 R 之后,編譯器將會報錯:Attribute value must be constant ,如下圖:

編譯器報錯

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

R中的屬性為變量

如何解決此問題?既然 R 不能滿足要求,那就自己構(gòu)建一個 R2,由 R 復(fù)制而來,并且將其屬性都修改為 public static final 來修飾的常量。為了讓使用者對整個過程無感知,因此使用 gradle 插件來解決這個需求,這也是 butterknife-gradle-plugin 工程的由來。

butterknife-gradle-plugin 有兩個重要的第三方依賴,分別是 javaparserjavapoet ,前者用于解析 Java 文件,也就是解析 R 文件,后者在前文中已經(jīng)濃彩重墨,用于將解析結(jié)果生成 R2 文件。

整個插件工程的源碼并不難理解,在生成 R2 文件時,要將屬性定義成 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)插件的知識筆者將在接下來的另外一篇關(guān)于 hugo 的源碼解析中介紹。

(七)有態(tài)度的 Lint 檢查

生成了 R2 文件后,會產(chǎn)生一個問題:該文件僅是為注解而用的,對開發(fā)者并沒有任何約束力,怎么防止開發(fā)者誤用?如:

int id = R2.id.footer;

如果寫代碼是應(yīng)付工作,如果工作是績效驅(qū)動,這類問題完全不需要考慮。但是,作為優(yōu)秀的、有態(tài)度的、有情懷的開源框架,JakeWharton 和 ButterKnife 給了我們榜樣,為了解決這個問題,butterknife-lint 工程應(yīng)運(yùn)而生。

從工程名來看,不難理解這工程的意義:一個靜態(tài)代碼檢查工具,用來驗證非法的 R2 引用。一旦在我們的業(yè)務(wù)項目里不小心引用了 R2 文件,當(dāng)執(zhí)行 Lint 后,將會有如下圖的提示信息:

Lint檢查非法的R2調(diào)用

追求完美的 JakeWharton ,有態(tài)度的 ButterKnife !

(八)總結(jié)

輪子天天有,但是好輪子并不常見。輪子的創(chuàng)意、價值、技術(shù)選型、單元測試以及追求完美的態(tài)度是衡量一個優(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容