0X0 前言
做過Android開發的猿類很多都知道ButterKnife這么個東西。這個庫可以大大的簡化我們的代碼,讓我們少寫很多findViewById
和setOnClickListener
這種代碼。網上有很多關于怎么使用這個庫的文章,并且很多人也知道這個庫的工作原理,但是關于這個庫的系統性的源碼分析的文章,似乎并不多。我在研究了一些這個庫的源碼之后,覺得還是有點意思的,在這里簡單的做一下分享。
ButterKnife工作分為三部分,
- 搜索代碼中被
@Bind
注解標記的元素(feild或method),將注解中的id號與元素建立對應關系 - 動態生成綁定操作依賴的代碼
- 在用戶調用的
bing
方法中執行最后的綁定
前兩步在預處理過程中執行,最后一步在app運行過程中由app代碼調用執行。
本文以項目源碼附帶的Sample工程的構建過程為例,依次分析各個步驟的執行過程。但在此之前我有義務先給出butterknife的官方源碼倉庫的地址:
由于這個開源項目的版本迭代速度很快,隨著時間的推移,本文中展示的代碼可能會因為過時而和官方倉庫中的產生較大出入,所以我再另外給出我寫這篇文章時從官方倉庫中fork出來的分支的地址:
以便讀者發現和官方最新的代碼有出入時做參考。
0X1 第一步:解析注解,生成對應關系
和很多其他Android第三方類庫不同,ButterKnife的大部分代碼執行在注解處理器中。注解處理器工作在整個Android工程的構建階段。我們可以把注解處理器理解為類似c語言構建過程中的預處理器的角色,它在編譯器被輸入源碼之前預先對代碼做了一些處理。
注解處理器需要在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中和綁定有關的。那么怎么理解這個函數返回的這些注解的作用呢?
通過反復的修改代碼驗證我發現,只有工程代碼中出現了此列表中注冊的注解,才會調用后續的process(Set<? extends TypeElement> elements, RoundEnvironment env)
方法。而process(Set<? extends TypeElement> elements, RoundEnvironment env)
方法中處理的注解和Set<String> ButterKnifeProcessor.getSupportedAnnotationTypes()
返回的注解列表并沒有直接的包含和被包含關系。
比如,我們可以讓getSupportedAnnotationTypes()
返回的Set中只包含Unbinder
,只要Sample工程中有使用到@Unbinder
注解的元素,之后調用的process(Set<? extends TypeElement> elements, RoundEnvironment env)
依然可以掃描出@Bind
注解的元素,并進行綁定。
相反,如果getSupportedAnnotationTypes()
返回的Set中的注解在Sample項目中并沒有被使用到,比如我們只返回BindArray
、 BindBitmap
、 BindBool
、 BindColor
、 BindDimen
、 BindDrawable
、 BindInt
、 BindString
這幾個,但是實際上Sample工程中并沒有使用到這幾個注解,那么javac就不會執行process(Set<? extends TypeElement> elements, RoundEnvironment env)
這個方法,綁定操作就會失效,應用啟動立即core dump。
如果getSupportedAnnotationTypes
方法的返回值檢查無誤,接下來編譯器會開始調用process(Set<? extends TypeElement> elements, RoundEnvironment env)
方法由此開始正式解析Sample工程中的注解。解析注解的任務主要由Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env)
這條語句來完成。
而在findAndParseTargets(env)
中,會多次調用
RoundEnvironment.getElementsAnnotatedWith(Class<? extends Annotation> a)
方法返回各類被注解的元素集合。比如,調用env.getElementsAnnotatedWith(Bind.class)
可以立刻獲得Sample工程中的所有被@Bind
注解的元素。
以Sample工程為例,以下是env.getElementsAnnotatedWith(Bind.class)
返回的結果。我刪除了一些無需關注的信息。
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循環中對env.getElementsAnnotatedWith(Bind.class)
返回集合中的每一個element
執行parseBind(element, targetClassMap, erasedTargetNames)
先劇透一下,當parseBind
返回時,targetClassMap
里面將會保存@Bind
注解中的value
(即布局文件中的view id)和被注解的元素的對應關系。下面讓我們按住Ctrl鍵單擊parseBind
,看看里面都做了什么。
在parseBind
當中,首先經過一系列的驗證排除掉對集合類元素的注解這種異常場景之后,最終會調用parseBindOne(element, targetClassMap, erasedTargetNames)
而在parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<TypeElement> erasedTargetNames)
中,又要對被注解的元素element
是否是安卓View的子類實例以及注解的value是否只有一個值進行校驗。也就是是說,通過@Bind
綁定的只能是View
,每一個View
只能綁定一個id。
校驗結束后,通過element
所屬的類enclosingElement
(通過element.getEnclosingElement()
獲取)為key到targetClassMap
中查找bindingClass
,對第一個element
進行解析的時候肯定是查找不到的,于是就會在getOrCreateTargetClass(targetClassMap, enclosingElement)
中創建一個BindingClass
類型的bindingClass
變量,并且以enclosingElement
為key添加到targetClassMap
中,后續的element
解析過程中會復用這個bindingClass
變量。
BindingClass
的精髓在于它的viewIdMap
成員變量,它是個Map<Integer, ViewBindings>
類型的變量,這個Map
變量保存了布局中的view id和Activity
中被@Bind
注解的View
類型及其子類型成員變量的對應關系。
成員變量信息保存在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);
進行構建,最后通過bindingClass.addField(id, binding);
添加到viewIdMap
當中。
以下為@Bind
注解解析完成后targetClassMap
的快照(篇幅有限,刪除了一些無關信息)
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實例的對應關系。接下來的任務就是動態的生成綁定依賴的代碼。
0X2 第二步:動態生成綁定依賴的代碼
在第一步的Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env)
中,我們得到了有很多個bindingClass
的map,其中每一個bindingClass
對應一個Sample工程中的涉及ButterKnife注解的類,其中包含了一系列需要ButterKnife處理的信息。這一步就根據這些信息調用bindingClass.brewJava()
動態生成綁定依賴的代碼。這里使用到了第三方類庫JavaPoet
還是以SimpleActivity
為例,以下代碼首先根據bindingClass.className
生成相關的類:
TypeSpec.Builder result = TypeSpec.classBuilder(className)
.addModifiers(PUBLIC)
.addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));
這里className
=SimpleActivity$$ViewBinder
,targetClass
=SimpleActivity
SimpleActivity
沒有父類,于是通過以下代碼給SimpleActivity$$ViewBinder
設置父類ViewBinder
result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
于是我們到目前為止有了一個動態生成的類SimpleActivity$$ViewBinder
,等價如下的代碼:
public class SimpleActivity$$ViewBinder<T extends SimpleActivity> implements ViewBinder<T> {
}
之后以下語句為這個動態生成的SimpleActivity$$ViewBinder
類新增bind
方法:
result.addMethod(createBindMethod());
打開createBindMethod()
的實現可以看到,首先構造了一個名為bind
的方法,該方法有一個@Override
注解,訪問級別為pulbic
,三個參數分別為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循環遍歷viewIdMap
,這個map中的每一個value就是一個在bindingClass
對應的Activity
中被@Binde
注解的View
類型成員變量。在這個for循環中,對每一個View
類型變量調用addViewBindings
方法,代碼如下:
for (ViewBindings bindings : viewIdMap.values()) {
addViewBindings(result, bindings);
}
在addViewBindings
方法中最終調用以下語句針對每一個View
類型變量生成具體的執行代碼(以下不是最終生成的代碼,而是控制生成代碼的代碼):
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
,其最終生成的執行代碼為
view = finder.findRequiredView(source, 2130968576, "field 'title'");
target.title = finder.castView(view, 2130968576, "field 'title'");
我們暫時可以先不用關心findRequiredView
和castView
這兩個方法的實現分別是什么,在這里,即抽象處理器中,它們暫時只是一段自動生成的,不會被編譯運行的文本而已。
等這些代碼都構建完成后,調用writeTo(filer);
將構建的類寫入文件,動態生成代碼的工作就完成了。
在這一步中,最終動態生成的源碼可在Sample工程構建完成后的build\generated\source\apt\debug\com\example\butterknife
目錄下找到。
0X3 第三步:運行時綁定
綁定操作位于app運行時。通常由在Activity
的onCreate
方法內調用ButterKnife.bind(this)
觸發執行。
打開ButterKnife
類的定義,可以看到有多個bind
方法的重載,Activity
中調用的重載版本是
public static void bind(@NonNull Activity target) {
bind(target, target, Finder.ACTIVITY);
}
注意這里的第三個參數Finder.ACTIVITY
,它是枚舉Finder
下的一個枚舉值,而Finder
中聲明了兩個抽象方法
protected abstract View findView(Object source, int id);
public abstract Context getContext(Object source);
又分別在包括Finder.ACTIVITY
在內的一系列枚舉值當中做了實現。所以我們實際上可以認為Finder
是一個抽象類,而Finder.ACTIVITY
,Finder.VIEW
,Finder.DIALOG
是這個抽象類的實例,他們各自對以上兩個抽象方法做了自己的實現。
后續的綁定流程中,Finder.ACTIVITY
會以Finder
類型的身份出現,當看到類似finder.findView(source, id)
這樣的語句時,我們就可以知道去哪里查看其內部實現。
在bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder)
中,首先根據target
的類型targetClass
,在這里即SimpleActivity
找到其對應的ViewBinder
,該操作位于findViewBinderForClass(targetClass)
中。
在findViewBinderForClass
方法中,針對每個targetClass
,如果是初次運行該方法,會通過Class.forName(String className)
方法動態加載其對應的ViewBinder
類。這里className
為targetClass
的名字和$$ViewBinder
拼接。以SimpleActivity
為例,取到的類就是com.example.butterknife.SimpleActivity$$ViewBinder
,即我們之前在build\generated\source\apt\debug\com\example\butterknife
下動態生成的類。
如果ViewBinder
類獲取成功,newInstance
方法獲取其實例,以targetClass
為key放入Map BINDERS
中,下次再找targetClass
對應的ViewBinder
類實例時可直接在BINDERS
中查找。最后返回這個ViewBinder
類的實例。
取到了對應的ViewBinder
實例之后,立即執行viewBinder.bind(finder, target, source)
這里的finder
是剛才的Finder.ACTIVITY
,target
和source
都是調用ButterKnife.bind
的SimpleActivity
實例。
這里的viewBinder.bind(finder, target, source);
執行的就是之前第二步中動態構造出來的方法,里面執行了一系列具體的view
綁定操作,就是我們在第二步中暫時不用關心的那兩行代碼:
view = finder.findRequiredView(source, 2130968576, "field 'title'");
target.title = finder.castView(view, 2130968576, "field 'title'");
現在我們需要了解這兩個代碼具體是怎么執行的綁定操作。
先看finder.findRequiredView(source, 2130968576, "field 'title'")
這個方法,它首先會調用Finder.ACTIVITY
中的findView(Object source, int id)
實現版本,可以看到該版本的findView(Object source, int id)
@Override protected View findView(Object source, int id) {
return ((Activity) source).findViewById(id);
}
將source
強轉為Activity
類型后調用了它的 findViewById(id)
方法,就是我們寫到吐的那個方法。該方法返回了一個View
類型變量。
如果我們手寫findViewById(id)
的話,通常會對其返回值進行一次類型轉換,轉換為這個View
的實際類型,比如最常見的TextView
,ListView
這種。而在這里,這種類型轉換在接下來的target.title = finder.castView(view, 2130968576, "field 'title'")
中進行(findRequiredView
方法內雖然自帶了一次castView
調用,但是findRequiredView
的返回值是View
類型,所以這里的類型轉換并沒有起作用)。
public <T> T castView(View view, int id, String who) {
try {
return (T) view;
} catch (ClassCastException e) {
// 處理一些幾乎不可能發生的異常情況
}
castView
方法會根據它的模板類型T
,即返回值的類型(此處為target.title
的類型TextView
)自行推斷需要將view
轉換的目標類型。最后將返回值賦值給target.title
即SimpleActivity
的title
成員變量,至此整個綁定操作完成。
到此為止,butterknife的工作就結束了。
0X4 結束前再寫點廢話
隨著SimpleActivity
的title
變量找到了它在布局文件中對應的TextView
,本文對butterknife的工作原理的源碼分析也告一段落。寫這篇文章的時間跨度實在有點長,3月份開始看butterknife的源碼并自行調試,4月份開始打草稿,懶癌發作拖拖拉拉搞到今天已經是5月底,再到github上一看發現本文中最為核心的@Bind
注解居然不見了,看的我一臉懵逼(后來仔細一看改名為@BindView
了),不得不佩服自己當初手快fork了一個分支出來的先見之明。現在還不知道距離兩個月前代碼官方又做了哪些修改,但是不管怎么改,工作原理應該還是沒有太大變化的。本文暫且還是基于7兩個月前的代碼,地址我已經在開頭貼出來了。
希望我的這篇文章對大家學習和使用ButterKnife,甚至根據自己的需要進行魔改有所幫助。