
0X0 前言
做過Android開發(fā)的猿類很多都知道ButterKnife這么個東西。這個庫可以大大的簡化我們的代碼,讓我們少寫很多findViewById和setOnClickListener這種代碼。網(wǎng)上有很多關(guān)于怎么使用這個庫的文章,并且很多人也知道這個庫的工作原理,但是關(guān)于這個庫的系統(tǒng)性的源碼分析的文章,似乎并不多。我在研究了一些這個庫的源碼之后,覺得還是有點意思的,在這里簡單的做一下分享。
ButterKnife工作分為三部分,
- 搜索代碼中被
@Bind注解標記的元素(feild或method),將注解中的id號與元素建立對應(yīng)關(guān)系 - 動態(tài)生成綁定操作依賴的代碼
- 在用戶調(diào)用的
bing方法中執(zhí)行最后的綁定
前兩步在預處理過程中執(zhí)行,最后一步在app運行過程中由app代碼調(diào)用執(zhí)行。
本文以項目源碼附帶的Sample工程的構(gòu)建過程為例,依次分析各個步驟的執(zhí)行過程。但在此之前我有義務(wù)先給出butterknife的官方源碼倉庫的地址:
由于這個開源項目的版本迭代速度很快,隨著時間的推移,本文中展示的代碼可能會因為過時而和官方倉庫中的產(chǎn)生較大出入,所以我再另外給出我寫這篇文章時從官方倉庫中fork出來的分支的地址:
以便讀者發(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項目中并沒有被使用到,比如我們只返回BindArray、 BindBitmap、 BindBool、 BindColor、 BindDimen、 BindDrawable、 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)心findRequiredView和castView這兩個方法的實現(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運行時。通常由在Activity的onCreate方法內(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.ACTIVITY,Finder.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類。這里className為targetClass的名字和$$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,target和source都是調(diào)用ButterKnife.bind的SimpleActivity實例。
這里的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.title即SimpleActivity的title成員變量,至此整個綁定操作完成。
到此為止,butterknife的工作就結(jié)束了。
0X4 結(jié)束前再寫點廢話
隨著SimpleActivity的title變量找到了它在布局文件中對應(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ù)自己的需要進行魔改有所幫助。