ButterKnife源碼剖析

logo.png

0X0 前言

做過Android開發(fā)的猿類很多都知道ButterKnife這么個東西。這個庫可以大大的簡化我們的代碼,讓我們少寫很多findViewByIdsetOnClickListener這種代碼。網(wǎng)上有很多關(guān)于怎么使用這個庫的文章,并且很多人也知道這個庫的工作原理,但是關(guān)于這個庫的系統(tǒng)性的源碼分析的文章,似乎并不多。我在研究了一些這個庫的源碼之后,覺得還是有點意思的,在這里簡單的做一下分享。

ButterKnife工作分為三部分,

  1. 搜索代碼中被@Bind注解標記的元素(feild或method),將注解中的id號與元素建立對應(yīng)關(guān)系
  2. 動態(tài)生成綁定操作依賴的代碼
  3. 在用戶調(diào)用的bing方法中執(zhí)行最后的綁定

前兩步在預處理過程中執(zhí)行,最后一步在app運行過程中由app代碼調(diào)用執(zhí)行。

本文以項目源碼附帶的Sample工程的構(gòu)建過程為例,依次分析各個步驟的執(zhí)行過程。但在此之前我有義務(wù)先給出butterknife的官方源碼倉庫的地址:

https://github.com/JakeWharton/butterknife

由于這個開源項目的版本迭代速度很快,隨著時間的推移,本文中展示的代碼可能會因為過時而和官方倉庫中的產(chǎn)生較大出入,所以我再另外給出我寫這篇文章時從官方倉庫中fork出來的分支的地址:

https://github.com/knightingal/butterknife

以便讀者發(fā)現(xiàn)和官方最新的代碼有出入時做參考。

0X1 第一步:解析注解,生成對應(yīng)關(guān)系

和很多其他Android第三方類庫不同,ButterKnife的大部分代碼執(zhí)行在注解處理器中。注解處理器工作在整個Android工程的構(gòu)建階段。我們可以把注解處理器理解為類似c語言構(gòu)建過程中的預處理器的角色,它在編譯器被輸入源碼之前預先對代碼做了一些處理。

注解處理器需要在META-INF中進行注冊才能工作。我們可以在butterknife-x.x.x.jar的/META-INF/services/javax.annotation.processing.Processor中看到注冊的注解處理器butterknife.compiler.ButterKnifeProcessor。該類繼承了jdk中的javax.annotation.processing.AbstractProcessor

ButterKnifeProcessor這個類覆蓋了AbstractProcessor中的4個方法,其中首先需要注意的是getSupportedAnnotationTypes這個方法。它返回了一系列本抽象處理器需要處理的注解,我們可以看到這些注解主要是ButterKnife中和綁定有關(guān)的。那么怎么理解這個函數(shù)返回的這些注解的作用呢?

通過反復的修改代碼驗證我發(fā)現(xiàn),只有工程代碼中出現(xiàn)了此列表中注冊的注解,才會調(diào)用后續(xù)的process(Set<? extends TypeElement> elements, RoundEnvironment env)方法。而process(Set<? extends TypeElement> elements, RoundEnvironment env)方法中處理的注解和Set<String> ButterKnifeProcessor.getSupportedAnnotationTypes()返回的注解列表并沒有直接的包含和被包含關(guān)系。

比如,我們可以讓getSupportedAnnotationTypes()返回的Set中只包含Unbinder,只要Sample工程中有使用到@Unbinder注解的元素,之后調(diào)用的process(Set<? extends TypeElement> elements, RoundEnvironment env)依然可以掃描出@Bind注解的元素,并進行綁定。

相反,如果getSupportedAnnotationTypes()返回的Set中的注解在Sample項目中并沒有被使用到,比如我們只返回BindArrayBindBitmap、 BindBoolBindColor、 BindDimenBindDrawable、 BindInt、 BindString這幾個,但是實際上Sample工程中并沒有使用到這幾個注解,那么javac就不會執(zhí)行process(Set<? extends TypeElement> elements, RoundEnvironment env)這個方法,綁定操作就會失效,應(yīng)用啟動立即core dump。

如果getSupportedAnnotationTypes方法的返回值檢查無誤,接下來編譯器會開始調(diào)用process(Set<? extends TypeElement> elements, RoundEnvironment env)方法由此開始正式解析Sample工程中的注解。解析注解的任務(wù)主要由Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env)這條語句來完成。

而在findAndParseTargets(env)中,會多次調(diào)用
RoundEnvironment.getElementsAnnotatedWith(Class<? extends Annotation> a)方法返回各類被注解的元素集合。比如,調(diào)用env.getElementsAnnotatedWith(Bind.class)可以立刻獲得Sample工程中的所有被@Bind注解的元素。

以Sample工程為例,以下是env.getElementsAnnotatedWith(Bind.class)返回的結(jié)果。我刪除了一些無需關(guān)注的信息。

elementsWithBind = {LinkedHashSet@9711}  size = 9
 0 = {Symbol$VarSymbol@9714} "title"
  name = {UnsharedNameTable$NameImpl@9732} "title"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 1 = {Symbol$VarSymbol@9715} "subtitle"
  name = {UnsharedNameTable$NameImpl@9739} "subtitle"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 2 = {Symbol$VarSymbol@9716} "hello"
  name = {UnsharedNameTable$NameImpl@9744} "hello"
  type = {Type$ClassType@9745} "android.widget.Button"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 3 = {Symbol$VarSymbol@9717} "listOfThings"
  name = {UnsharedNameTable$NameImpl@9750} "listOfThings"
  type = {Type$ClassType@9751} "android.widget.ListView"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 4 = {Symbol$VarSymbol@9718} "footer"
  name = {UnsharedNameTable$NameImpl@9756} "footer"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 5 = {Symbol$VarSymbol@9719} "headerViews"
  name = {UnsharedNameTable$NameImpl@9761} "headerViews"
  type = {Type$ClassType@9762} "java.util.List<android.view.View>"
  owner = {Symbol$ClassSymbol@9734} "com.example.butterknife.SimpleActivity"
 6 = {Symbol$VarSymbol@9720} "word"
  name = {UnsharedNameTable$NameImpl@9767} "word"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9768} "com.example.butterknife.SimpleAdapter.ViewHolder"
 7 = {Symbol$VarSymbol@9721} "length"
  name = {UnsharedNameTable$NameImpl@9773} "length"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9768} "com.example.butterknife.SimpleAdapter.ViewHolder"
 8 = {Symbol$VarSymbol@9722} "position"
  name = {UnsharedNameTable$NameImpl@9778} "position"
  type = {Type$ClassType@9733} "android.widget.TextView"
  owner = {Symbol$ClassSymbol@9768} "com.example.butterknife.SimpleAdapter.ViewHolder"

該集合中的元素屬性主要包括元素的名字,類型,所屬的類。

之后在一個foreach循環(huán)中對env.getElementsAnnotatedWith(Bind.class)返回集合中的每一個element執(zhí)行parseBind(element, targetClassMap, erasedTargetNames)

先劇透一下,當parseBind返回時,targetClassMap里面將會保存@Bind注解中的value(即布局文件中的view id)和被注解的元素的對應(yīng)關(guān)系。下面讓我們按住Ctrl鍵單擊parseBind,看看里面都做了什么。

parseBind當中,首先經(jīng)過一系列的驗證排除掉對集合類元素的注解這種異常場景之后,最終會調(diào)用parseBindOne(element, targetClassMap, erasedTargetNames)

而在parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<TypeElement> erasedTargetNames)中,又要對被注解的元素element是否是安卓View的子類實例以及注解的value是否只有一個值進行校驗。也就是是說,通過@Bind綁定的只能是View,每一個View只能綁定一個id。

校驗結(jié)束后,通過element所屬的類enclosingElement(通過element.getEnclosingElement()獲?。閗ey到targetClassMap中查找bindingClass,對第一個element進行解析的時候肯定是查找不到的,于是就會在getOrCreateTargetClass(targetClassMap, enclosingElement)中創(chuàng)建一個BindingClass類型的bindingClass變量,并且以enclosingElement為key添加到targetClassMap中,后續(xù)的element解析過程中會復用這個bindingClass變量。

BindingClass的精髓在于它的viewIdMap成員變量,它是個Map<Integer, ViewBindings>類型的變量,這個Map變量保存了布局中的view id和Activity中被@Bind注解的View類型及其子類型成員變量的對應(yīng)關(guān)系。
成員變量信息保存在ViewBindings的子類FieldViewBinding中。FieldViewBinding的實例通過parseBindOne方法中的最后幾行代碼

String name = element.getSimpleName().toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);

FieldViewBinding binding = new FieldViewBinding(name, type, required);

進行構(gòu)建,最后通過bindingClass.addField(id, binding);添加到viewIdMap當中。

以下為@Bind注解解析完成后targetClassMap的快照(篇幅有限,刪除了一些無關(guān)信息)

targetClassMap = {LinkedHashMap@9687}  size = 2
+-0 = {LinkedHashMap$Entry@9751}
| +-key = {Symbol$ClassSymbol@9753} "com.example.butterknife.SimpleActivity"
| +-value = {BindingClass@9754}
|   +-viewIdMap = {LinkedHashMap@9760}  size = 5
|   | +-0 = {LinkedHashMap$Entry@9770} "2130968576" ->
|   | |  key = {Integer@9775} "2130968576"
|   | |  value = {ViewBindings@9776}
|   | |   id = 2130968576
|   | |   fieldBindings = {LinkedHashSet@9791}  size = 1
|   | |    0 = {FieldViewBinding@9794}
|   | |     name = {String@9795} "title"
|   | |     type = {ClassName@9796} "android.widget.TextView"
|   | +-1 = {LinkedHashMap$Entry@9771} "2130968577" ->
|   | |  key = {Integer@9777} "2130968577"
|   | |  value = {ViewBindings@9778}
|   | |   id = 2130968577
|   | |   fieldBindings = {LinkedHashSet@9800}  size = 1
|   | |    0 = {FieldViewBinding@9803}
|   | |     name = {String@9804} "subtitle"
|   | |     type = {ClassName@9805} "android.widget.TextView"
|   | +-2 = {LinkedHashMap$Entry@9772} "2130968578" ->
|   | |  key = {Integer@9779} "2130968578"
|   | |  value = {ViewBindings@9780}
|   | |   id = 2130968578
|   | |   fieldBindings = {LinkedHashSet@9808}  size = 1
|   | |    0 = {FieldViewBinding@9811}
|   | |     name = {String@9812} "hello"
|   | |     type = {ClassName@9813} "android.widget.Button"
|   | +-3 = {LinkedHashMap$Entry@9773} "2130968579" ->
|   | |  key = {Integer@9781} "2130968579"
|   | |  value = {ViewBindings@9782}
|   | |   id = 2130968579
|   | |   fieldBindings = {LinkedHashSet@9816}  size = 1
|   | |    0 = {FieldViewBinding@9819}
|   | |     name = {String@9820} "listOfThings"
|   | |     type = {ClassName@9821} "android.widget.ListView"
|   | +-4 = {LinkedHashMap$Entry@9774} "2130968580" ->
|   |    key = {Integer@9783} "2130968580"
|   |    value = {ViewBindings@9784}
|   |     id = 2130968580
|   |     fieldBindings = {LinkedHashSet@9824}  size = 1
|   |      0 = {FieldViewBinding@9827}
|   |       name = {String@9828} "footer"
|   |       type = {ClassName@9829} "android.widget.TextView"
|   +-classPackage = {String@9765} "com.example.butterknife"
|   +-className = {String@9766} "SimpleActivity$$ViewBinder"   
+-1 = {LinkedHashMap$Entry@9752}
  +-key = {Symbol$ClassSymbol@9755} "com.example.butterknife.SimpleAdapter.ViewHolder"
  +-value = {BindingClass@9756}
    +-viewIdMap = {LinkedHashMap@9832}  size = 3
    | +-0 = {LinkedHashMap$Entry@9842} "2130968581" ->
    | |  key = {Integer@9845} "2130968581"
    | |  value = {ViewBindings@9846}
    | |   id = 2130968581
    | |   fieldBindings = {LinkedHashSet@9855}  size = 1
    | |    0 = {FieldViewBinding@9858}
    | |     name = {String@9859} "word"
    | |     type = {ClassName@9860} "android.widget.TextView"
    | +-1 = {LinkedHashMap$Entry@9843} "2130968582" ->
    | |  key = {Integer@9847} "2130968582"
    | |  value = {ViewBindings@9848}
    | |   id = 2130968582
    | |   fieldBindings = {LinkedHashSet@9863}  size = 1
    | |    0 = {FieldViewBinding@9866}
    | |     name = {String@9867} "length"
    | |     type = {ClassName@9868} "android.widget.TextView"
    | +-2 = {LinkedHashMap$Entry@9844} "2130968583" ->
    |    key = {Integer@9849} "2130968583"
    |    value = {ViewBindings@9850}
    |     id = 2130968583
    |     fieldBindings = {LinkedHashSet@9871}  size = 1
    |      0 = {FieldViewBinding@9874}
    |       name = {String@9875} "position"
    |       type = {ClassName@9876} "android.widget.TextView"
    +-classPackage = {String@9837} "com.example.butterknife"
    +-className = {String@9838} "SimpleAdapter$ViewHolder$$ViewBinder"   

我們可以從targetClassMap中讀出諸如此類的以下信息:

  • 有兩個類中存在@Bind注解
  • 第一個類為com.example.butterknife.SimpleActivity
  • 該類中有5個被@Bind注解的成員
    
  • 第一個成員名字是title,類型是android.widget.TextView,綁定至id號為2130968576
    
  • 。。。依此類推

至此,對ButterKnife的@Bind注解解析完成,并在targetClassMap中建立起了view id和view實例的對應(yīng)關(guān)系。接下來的任務(wù)就是動態(tài)的生成綁定依賴的代碼。

0X2 第二步:動態(tài)生成綁定依賴的代碼

在第一步的Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env)中,我們得到了有很多個bindingClass的map,其中每一個bindingClass對應(yīng)一個Sample工程中的涉及ButterKnife注解的類,其中包含了一系列需要ButterKnife處理的信息。這一步就根據(jù)這些信息調(diào)用bindingClass.brewJava()動態(tài)生成綁定依賴的代碼。這里使用到了第三方類庫JavaPoet

還是以SimpleActivity為例,以下代碼首先根據(jù)bindingClass.className生成相關(guān)的類:

TypeSpec.Builder result = TypeSpec.classBuilder(className)
        .addModifiers(PUBLIC)
        .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));

這里className=SimpleActivity$$ViewBinder,targetClass=SimpleActivity

SimpleActivity沒有父類,于是通過以下代碼給SimpleActivity$$ViewBinder設(shè)置父類ViewBinder

  result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));

于是我們到目前為止有了一個動態(tài)生成的類SimpleActivity$$ViewBinder,等價如下的代碼:

public class SimpleActivity$$ViewBinder<T extends SimpleActivity> implements ViewBinder<T> {

}

之后以下語句為這個動態(tài)生成的SimpleActivity$$ViewBinder類新增bind方法:

  result.addMethod(createBindMethod());

打開createBindMethod()的實現(xiàn)可以看到,首先構(gòu)造了一個名為bind的方法,該方法有一個@Override注解,訪問級別為pulbic,三個參數(shù)分別為final Finder finder, final T target, Object source。

MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
        .addAnnotation(Override.class)
        .addModifiers(PUBLIC)
        .addParameter(FINDER, "finder", FINAL)
        .addParameter(TypeVariableName.get("T"), "target", FINAL)
        .addParameter(Object.class, "source");

接下來用一個for循環(huán)遍歷viewIdMap,這個map中的每一個value就是一個在bindingClass對應(yīng)的Activity中被@Binde注解的View類型成員變量。在這個for循環(huán)中,對每一個View類型變量調(diào)用addViewBindings方法,代碼如下:

for (ViewBindings bindings : viewIdMap.values()) {
        addViewBindings(result, bindings);
}

addViewBindings方法中最終調(diào)用以下語句針對每一個View類型變量生成具體的執(zhí)行代碼(以下不是最終生成的代碼,而是控制生成代碼的代碼):

result.addStatement("view = finder.findRequiredView(source, $L, $S)", bindings.getId(),
            asHumanDescription(requiredViewBindings));
result.addStatement("target.$L = finder.castView(view, $L, $S)", fieldBinding.getName(),
            bindings.getId(), asHumanDescription(fieldBindings));

比如對于SimpleActivity中的成員變量title,其最終生成的執(zhí)行代碼為

view = finder.findRequiredView(source, 2130968576, "field 'title'");
target.title = finder.castView(view, 2130968576, "field 'title'");

我們暫時可以先不用關(guān)心findRequiredViewcastView這兩個方法的實現(xiàn)分別是什么,在這里,即抽象處理器中,它們暫時只是一段自動生成的,不會被編譯運行的文本而已。

等這些代碼都構(gòu)建完成后,調(diào)用writeTo(filer);將構(gòu)建的類寫入文件,動態(tài)生成代碼的工作就完成了。

在這一步中,最終動態(tài)生成的源碼可在Sample工程構(gòu)建完成后的build\generated\source\apt\debug\com\example\butterknife目錄下找到。

0X3 第三步:運行時綁定

綁定操作位于app運行時。通常由在ActivityonCreate方法內(nèi)調(diào)用ButterKnife.bind(this)觸發(fā)執(zhí)行。

打開ButterKnife類的定義,可以看到有多個bind方法的重載,Activity中調(diào)用的重載版本是

public static void bind(@NonNull Activity target) {
  bind(target, target, Finder.ACTIVITY);
}

注意這里的第三個參數(shù)Finder.ACTIVITY,它是枚舉Finder下的一個枚舉值,而Finder中聲明了兩個抽象方法

protected abstract View findView(Object source, int id);

public abstract Context getContext(Object source);

又分別在包括Finder.ACTIVITY在內(nèi)的一系列枚舉值當中做了實現(xiàn)。所以我們實際上可以認為Finder是一個抽象類,而Finder.ACTIVITYFinder.VIEW,Finder.DIALOG是這個抽象類的實例,他們各自對以上兩個抽象方法做了自己的實現(xiàn)。

后續(xù)的綁定流程中,Finder.ACTIVITY會以Finder類型的身份出現(xiàn),當看到類似finder.findView(source, id)這樣的語句時,我們就可以知道去哪里查看其內(nèi)部實現(xiàn)。

bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder)中,首先根據(jù)target的類型targetClass,在這里即SimpleActivity找到其對應(yīng)的ViewBinder,該操作位于findViewBinderForClass(targetClass)中。

findViewBinderForClass方法中,針對每個targetClass,如果是初次運行該方法,會通過Class.forName(String className)方法動態(tài)加載其對應(yīng)的ViewBinder類。這里classNametargetClass的名字和$$ViewBinder拼接。以SimpleActivity為例,取到的類就是com.example.butterknife.SimpleActivity$$ViewBinder,即我們之前在build\generated\source\apt\debug\com\example\butterknife下動態(tài)生成的類。

如果ViewBinder類獲取成功,newInstance方法獲取其實例,以targetClass為key放入Map BINDERS中,下次再找targetClass對應(yīng)的ViewBinder類實例時可直接在BINDERS中查找。最后返回這個ViewBinder類的實例。

取到了對應(yīng)的ViewBinder實例之后,立即執(zhí)行viewBinder.bind(finder, target, source)這里的finder是剛才的Finder.ACTIVITY,targetsource都是調(diào)用ButterKnife.bindSimpleActivity實例。

這里的viewBinder.bind(finder, target, source);執(zhí)行的就是之前第二步中動態(tài)構(gòu)造出來的方法,里面執(zhí)行了一系列具體的view綁定操作,就是我們在第二步中暫時不用關(guān)心的那兩行代碼:

view = finder.findRequiredView(source, 2130968576, "field 'title'");
target.title = finder.castView(view, 2130968576, "field 'title'");

現(xiàn)在我們需要了解這兩個代碼具體是怎么執(zhí)行的綁定操作。

先看finder.findRequiredView(source, 2130968576, "field 'title'")這個方法,它首先會調(diào)用Finder.ACTIVITY中的findView(Object source, int id)實現(xiàn)版本,可以看到該版本的findView(Object source, int id)

@Override protected View findView(Object source, int id) {
    return ((Activity) source).findViewById(id);
}

source強轉(zhuǎn)為Activity類型后調(diào)用了它的 findViewById(id)方法,就是我們寫到吐的那個方法。該方法返回了一個View類型變量。

如果我們手寫findViewById(id)的話,通常會對其返回值進行一次類型轉(zhuǎn)換,轉(zhuǎn)換為這個View的實際類型,比如最常見的TextView,ListView這種。而在這里,這種類型轉(zhuǎn)換在接下來的target.title = finder.castView(view, 2130968576, "field 'title'")中進行(findRequiredView方法內(nèi)雖然自帶了一次castView調(diào)用,但是findRequiredView的返回值是View類型,所以這里的類型轉(zhuǎn)換并沒有起作用)。

public <T> T castView(View view, int id, String who) {
    try {
      return (T) view;
    } catch (ClassCastException e) {
      // 處理一些幾乎不可能發(fā)生的異常情況
  }

castView方法會根據(jù)它的模板類型T,即返回值的類型(此處為target.title的類型TextView)自行推斷需要將view轉(zhuǎn)換的目標類型。最后將返回值賦值給target.titleSimpleActivitytitle成員變量,至此整個綁定操作完成。

到此為止,butterknife的工作就結(jié)束了。

0X4 結(jié)束前再寫點廢話

隨著SimpleActivitytitle變量找到了它在布局文件中對應(yīng)的TextView,本文對butterknife的工作原理的源碼分析也告一段落。寫這篇文章的時間跨度實在有點長,3月份開始看butterknife的源碼并自行調(diào)試,4月份開始打草稿,懶癌發(fā)作拖拖拉拉搞到今天已經(jīng)是5月底,再到github上一看發(fā)現(xiàn)本文中最為核心的@Bind注解居然不見了,看的我一臉懵逼(后來仔細一看改名為@BindView了),不得不佩服自己當初手快fork了一個分支出來的先見之明?,F(xiàn)在還不知道距離兩個月前代碼官方又做了哪些修改,但是不管怎么改,工作原理應(yīng)該還是沒有太大變化的。本文暫且還是基于7兩個月前的代碼,地址我已經(jīng)在開頭貼出來了。

希望我的這篇文章對大家學習和使用ButterKnife,甚至根據(jù)自己的需要進行魔改有所幫助。

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

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

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