拆 JakeWharton 系列之 ButterKnife

JakeWharton 是 Android 大神,同時也是開源狂魔。他開源的項目特色是小而美,且應用普遍,好比 butterknifeRxBindinghugo 等,本文從受衆最普遍,star 最多的 ButterKnife 講起。html

(一) 你將得到什麼

經過閱讀 ButterKnife 源碼和本文,你將收穫:java

  • android-apt 三件套:
    1. 註解處理器(AbstractProcess)
    2. 註解處理器註冊(AutoService)
    3. 代碼生成(JavaPoet)
  • 自定義 gradle 插件
  • 造一個優秀輪子應該具有的態度

(二)ButterKnife 簡介

ButterKnife 使用註解的方式來替代繁瑣的 findViewById 和註冊監聽器時大量的匿名內部類寫法。android

本文正對 8.5.1 版本的源碼進行分析,自從 8.2.0 起已經支持 library 工程。github 地址爲:github.com/JakeWharton…git

(三)ButterKnife 總覽

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

組件依賴關係

ButterKnife 共7個組件,他們的依賴關係以下圖所示(其中,butterknife-integration-test 工程不作介紹):api

butterknife組件依賴

  • 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

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

### 總體流程框架

butterknife-流程圖

將整個流程拆分紅編譯期間和運行期間,就不難理解 ButterKnife 的運行機制。伴隨而來的幾個問題:

  1. 編譯期間如何處理註解的信息,並解析生成 Java 文件?
  2. 運行期間如何綁定 Activity 中 的View 控件?
  3. 由 R 生成 R2 的意義是什麼?

(四)android-apt(Annotation Processing Tool)

首先來解決第一個問題,編譯期間註解處理,經過這兩個關鍵詞,咱們能夠聯想到的技術方案是: APT(Annotation Processing Tool),即註解處理工具。在該方案中,一般有個必備的三件套,分別是註解處理器 Processor,註冊註解處理器 AutoService 和代碼生成工具 JavaPoet。

三件套之註解處理器

ButterKnife 一切皆註解,所以首先須要個處理器來解析註解。 ButterKnifeProcessor 充當了該角色,其中 process 方法是觸發註解解析的入口,全部的神奇的事情從這裏發生。

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

  1. 解析全部包含了 ButterKnife 註解的類
  2. 根據解析結果,使用 JavaPoet 生成相應的Java文件

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

生成的 java 文件

Processor 是爲三件套之一。

小插曲之 UT

在介紹餘下二件套以前,先插播個小插曲,關於單元測試。

在閱讀源碼過程當中,debug 斷點工具每每能夠幫助咱們事半功倍,運行時的 debug 比較好處理,可是相似於 ButterKnife 這種須要在編譯期間處理邏輯的代碼應該如何進行 debug ?

單元測試能夠把代碼獨立成一個單元,而且能夠隔離對上下文、對環境的依賴(好比 Robolectric 對 Android 的 mock)。一個優秀的有態度的開源框架,每每都配備了齊全的單元測試,ButterKnife 也不例外。

butterknife 子組件中配備了大量的單元測試,這些單元測試是爲 ButterKnifeProcessor 量身打造的。好比 ExtendActivityTest 中的 views() 對 Activity 包含@BindView 的註解時的處理作了單元測試,運行 UT 後,能夠隨意斷點,以下圖:

對ButterKnifeProcessor斷點調試

建議讀者用這種方式來理解 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 ,最好的方式即是看官方文檔。簡而言之,當咱們寫一個類時,實際上是有固定結構的,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 的廣泛套路。

(六)支持 library

編譯時和運行時的問題解決了,還有最後一個問題:由 R 生成 R2 的意義是什麼?

若是你細心的話會發如今官方的 sample-library 中,註解的值均是由 R2 來引用的,以下圖:

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中的屬性爲變量

如何解決此問題?既然 R 不能知足要求,那就本身構建一個 R2,由 R 複製而來,而且將其屬性都修改成 public static final 來修飾的常量。爲了讓使用者對整個過程無感知,所以使用 gradle 插件來解決這個需求,這也是 butterknife-gradle-plugin 工程的由來。

butterknife-gradle-plugin 有兩個重要的第三方依賴,分別是 javaparserjavapoet ,前者用於解析 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 的源碼解析中介紹。

(七)有態度的 Lint 檢查

生成了 R2 文件後,會產生一個問題:該文件僅是爲註解而用的,對開發者並無任何約束行爲,怎麼防止開發者誤用?如:

int id = R2.id.footer;複製代碼

若是寫代碼是應付工做,若是工做是績效驅動,這類問題徹底不須要考慮。可是,做爲優秀的、有態度的、有情懷的開源框架,JakeWharton 和 ButterKnife 給了咱們榜樣,爲了解決這個問題,butterknife-lint 工程應運而生。

從工程名來看,不難理解這工程的意義:一個靜態代碼檢查工具,用來驗證非法的 R2 引用。一旦在咱們的業務項目裏不當心引用了 R2 文件,當執行 Lint 後,將會有以下圖的提示信息:

Lint檢查非法的R2調用

追求完美的 JakeWharton ,有態度的 ButterKnife !

(八)總結

輪子每天有,可是好輪子並不常見。輪子的創意、價值、技術選項、單元測試以及追求完美的態度是衡量一個優秀輪子的維度。ButterKnife 完美地詮釋了這一切。

參考文章

blog.stablekernel.com/the-10-step…
github.com/google/auto…
github.com/square/java…

相關文章
相關標籤/搜索