拆 Jake Wharton 系列之 ButterKnife

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

(一) 你將獲得什么

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

  • android-apt 三件套:
  1. 注解處理器(AbstractProcess)
  2. 注解處理器注冊(cè)(AutoService)
  3. 代碼生成(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 工程不做介紹):

butterknife組件依賴
  • 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ā) ActivityView 控件綁定的時(shí)機(jī)。
  • 2.butterknife-compiler:見名知意,編譯期間將使用該工程,他的作用是解析注解,并且生成 ActivityView 綁定的 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ì)介紹。

整體流程

butterknife-流程圖

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

  1. 編譯期間如何處理注解的信息,并解析生成 Java 文件?
  2. 運(yùn)行期間如何綁定 Activity 中 的View 控件?
  3. 由 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 方法中主要做兩件事情,分別是:

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

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
生成的 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),如下圖:

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

建議讀者用這種方式來理解 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 來引用的,如下圖:


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 ,如下圖:

編譯器報(bào)錯(cuò)

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

R中的屬性為變量

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

butterknife-gradle-plugin 有兩個(gè)重要的第三方依賴,分別是 javaparserjavapoet ,前者用于解析 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ì)有如下圖的提示信息:

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

追求完美的 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

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

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,725評(píng)論 25 709
  • 1、Butterknife是什么? Butterknife是一個(gè)依托Java的注解機(jī)制來實(shí)現(xiàn)輔助代碼生成的框架 2...
    DevWang閱讀 1,794評(píng)論 0 52
  • 附上原文作者連接:作者:金誠 一.榜單介紹 排行榜包括四大類: 單一框架:僅提供路由、網(wǎng)絡(luò)層、UI層、通信層或其他...
    這個(gè)美嘉不姓陳閱讀 2,326評(píng)論 1 35
  • 志氣與孝順,在我讀到與這句話相關(guān)的那篇文章時(shí),眼睛是濕潤的。因?yàn)樵诮裉?,已?jīng)沒有多少人能保持那份可貴的志氣與孝順了...
    镕镋子閱讀 528評(píng)論 2 3
  • 語文 英語
    A天之驕子閱讀 228評(píng)論 0 0

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