淺析 ButterKnife
不管是Android開發的老司機也好,新司機也罷,想必大家都對 findViewById 這種樣板代碼感到了厭倦,特別是進行復雜的UI界面開發的時候,這種代碼就會顯的非常的臃腫,既影響開發時的效率,又影響美觀。
俗話說,不想偷懶的程序猿不叫工程師,那有什么方法可以讓我們寫這樣的代碼更加的有效率呢?
使用依賴注入框架
如果你不想寫那些無聊的樣板代碼,那么你可以嘗試一下現有的依賴注入庫。 ButterKnife 作為Jake Wharton大神寫的開源框架,號稱在編譯期間就可以實現依賴注入,沒有用到反射,不會降低程序性能等。那么問題來了,它到底是怎么做到的呢?
初探ButterKnife
ButterKnife 是Jake Wharton寫的開源依賴注入框架,它和 Android Annotations 比較類似,都是用到了Java Annotation Tool來在編譯期間生成輔助代碼來達到View注入的目的。
注解處理器是Java1.5引入的工具,它提供了在程序編譯期間掃描和處理注解的能力。它的原理就是在編譯期間讀取Java代碼,解析注解,然后動態生成Java代碼。下圖是Java編譯代碼的流程,可以看到,我們的注解處理器的工作在Annotation Processing階段,最終通過注解處理器生成的代碼會和源代碼一起被編譯成Java字節碼。不過比較遺憾的是你不能修改已經存在的Java文件,比如在已經存在的類中添加新的方法,所以通過Java Annotation Tool只能通過輔助類的方式來實現View的依賴注入,這樣會略微增加項目的方法數和類數,不過只要控制好,不會對項目有太大的影響

ButterKnife 在業務層的使用我就不介紹了,各位老司機肯定是輕車熟路。假如是我們自己寫類似于 ButterKnife 這樣的框架,那么我們的思路是這樣:定義注解,掃描注解,生成代碼。同時,我們需要用到以下這幾個工具:JavaPoet(你當然可以直接用Java Annotation Tool,然后直接通過字符串拼接的方式去生成java源碼,如果你生無可戀的話),Java Annotation Tool以及APT插件。為了后續更好的閱讀 ButterKnife 的源碼,我們先來介紹一下JavaPoet的基礎知識。
JavaPoet生成代碼
JavaPoet是一個可以生成 .java 源代碼的開源項目,也是出自JakeWharton之手,我們可以結合注解處理器在程序編譯階段動態生成我們需要的代碼。先介紹一個使用JavaPoet最基本的例子:

其中:
-
MethodSpec:代表一個構造函數或者方法聲明
-
TypeSpec:代表一個類、接口或者枚舉聲明
-
FieldSpec:代表一個成員變量聲明
-
JavaFile:代表一個頂級的JAVA文件
運行結果:

是不是很神奇?我們的例子只是把生成的代碼寫到了輸出臺, ButterKnife 通過Java Annotation Tool的 Filer 可以幫助我們以文件的形式輸出JAVA源碼。問:那如果我要生成下面這段代碼,我們會怎么寫?

很簡單嘛,依葫蘆畫瓢,只要把 MethodSpec 替換成下面這段:

然后代碼華麗的生成了:

唉,等等,好像哪里不對啊,生成代碼的格式怎么這么奇怪!難道我要這樣寫嘛:

這樣寫肯定是能達到我們的要求,但是未免也太麻煩了一點。其實JavaPoet提供了一個 addStatement 接口,可以自動幫我們換行以及添加分號,那么我們的代碼就可以寫成這個樣子:

生成的代碼:

好吧,其實格式也不是那么好看對不對?而且還要 addStatement 還需要夾雜 addCode 一起使用。為什么寫個for循環都這么難(哭泣臉)。其實JavaPoet早考慮到這個問題,它提供了 beginControlFlow() + endControlFlow() 兩個接口提供換行和縮進,再結合負責分號和換行的 addStatement() ,我們的代碼就可以寫成這樣子:

生成的代碼相當的順眼:

其實JavaPoet還提供了很多有用的接口來幫我們更方便的生成代碼。
Java Annotation Tool
那么 ButterKnife 又是怎么通過Java Annotation Tool來生成我們的輔助代碼呢?讓我們以 ButterKnife 最新版本8.4.0的源代碼為例。假如是我們自己寫 ButterKnife 這樣的框架,那么第一步肯定得先定義自己的注解。在 ButterKnife 源碼的butterknife-annotations包中,我們可以看到 ButterKnife 自定義的所有的注解,如下圖所示。

有了自定義注解,那我們的下一步就是實現自己的注解處理器了。我們結合 ButterKnife 的 ButterKnifeProcessor 類來學習一下注解處理器的相關知識。為了實現自定義注解處理器,必須先繼承AbstractProcessor類。 ButterKnifeProcessor 通過繼承 AbstractProcessor ,實現了四個方法,如下圖所示:

-
init(ProcessingEnvironment env)
通過輸入 ProcessingEnvironment 參數,你可以在得到很多有用的工具類,比如 Elements , Types , Filer 等。
Elements 是可以用來處理 Element 的工具類,可以理解為Java Annotation Tool掃描過程中掃描到的所有的元素,比如包(PackageElement)、類(TypeElement)、方法(ExecuteableElement)等
Types 是可以用來處理 TypeMirror 的工具類,它代表在JAVA語言中的一種類型,我們可以通過 TypeMirror 配合 Elements 來判斷某個元素是否是我們想要的類型
Filer 是生成JAVA源代碼的工具類,能不能生成java源碼就靠它啦
-
getSupportedAnnotationTypes()
代表注解處理器可以支持的注解類型,由前面的分析可以知道, ButterKnife 支持的注解有 BindView 、 OnClick 等。 -
getSupportedSourceVersion()
支持的JDK版本,一般使用 SourceVersion.latestSupported() ,這里使用 Collections.singleton(OPTION_SDK_INT) 也是可以的。 -
process(Set<? extends TypeElement> elements, RoundEnvironment env)
process 是整個注解處理器的重頭戲,你所有掃描和處理注解的代碼以及生成Java源文件的代碼都寫在這里面,這個也是我們將要重點分析的方法。
ButterKnifeProcessor 的 process 方法看起來很簡單,實際上做了很多事情,大致可以分為兩個部分:
-
掃描所有的 ButterKnife 注解,并且生成以 TypeElement 為Key, BindingSet 為鍵值的HashMap。 TypeElement 我們在前面知道屬于類或者接口, BindingSet 用來記錄我們使用JavaPoet生成代碼時的一些參數,最終把該HashMap返回。這些邏輯對應于源碼中的 findAndParseTargets(RoundEnvironment env) 方法
-
生成輔助類。輔助類以 _ViewBinding 為后綴,比如在 MainActivity 中使用了 ButterKnife 注解,那么最終會生成 MainActivity_ViewBinding 輔助類。 MainActivity_ViewBinding 類中最終會生成對應于@BindView的 findViewById 等代碼。
第一步,我們先來分析 findAndParseTargets(RoundEnvironment env) 源碼。由于方法太長,而且做的事情都差不多,我們只需要分析一小段即可。
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
--- 省略部分代碼---
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
//遍歷所有被BindView注解的類
parseBindView(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
--- 省略部分代碼---
// Try to find a parent binder for each.
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
TypeElement parentType = findParentType(entry.getKey(), erasedTargetNames);
if (parentType != null) {
BindingClass bindingClass = entry.getValue();
BindingClass parentBindingClass = targetClassMap.get(parentType);
bindingClass.setParent(parentBindingClass);
}
}
return targetClassMap;
}
遍歷找到被注解的 Element 之后,通過 parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames) 方法去解析各個 Element 。在 parseBindView 方法中,首先會去檢測被注解的元素是不是View或者 Interface ,如果滿足條件則去獲取被注解元素的注解的值,如果相應的的 BindingSet.Builder 沒有被綁定過,那么通過 getOrCreateBindingBuilder 方法生成或者直接從 targetClassMap 中獲取(為了提高效率,生成的 BindingSet.Builder 會被存儲在 targetClassMap 中)。 getOrCreateBindingBuilder 方法比較簡單,我就不貼代碼了,生成的 BindingSet.Builder 會記錄一個值 binderClassName , ButterKnife 最終會根據 binderClassName 作為輔助類的類名。
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Start by verifying common generated code restrictions.
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
|| isBindingInWrongPackage(BindView.class, element);
// Verify that the target type extends from View.
TypeMirror elementType = element.asType();
--- 省略類型校驗邏輯的代碼---
// 獲取注解的值
int id = element.getAnnotation(BindView.class).value();
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder != null) {
ViewBindings viewBindings = builder.getViewBinding(getId(id));
if (viewBindings != null && viewBindings.getFieldBinding() != null) {
FieldViewBinding existingBinding = viewBindings.getFieldBinding();
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
BindView.class.getSimpleName(), id, existingBinding.getName(),
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
} else {
//如果沒有綁定過,那么通過該方法獲得對應的builder并且返回。這里的targetClassMap會存儲已經生成的builder,必要的時候提高效率
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
String name = element.getSimpleName().toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
builder.addField(getId(id), new FieldViewBinding(name, type, required));
erasedTargetNames.add(enclosingElement);
}
parseBindView 以及 findAndParseTargets 的解析工作完成后,所有的解析結果都會存放在 targetClassMap 中作為結果返回。我們現在來看 process 第二步的處理過程:遍歷 targetClassMap 中所有的 builder ,并且通過 Filer 生成JAVA源文件。
---代碼省略---
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
那么生成的代碼都長什么樣子呢?讓我們打開 BindingSet 的 brewJava(int sdk) 方法一探究竟。
JavaFile brewJava(int sdk) {
return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}

納尼,竟然這么簡單?我們觀察到 JavaFile 的靜態方法 builder(String packageName, TypeSpec typeSpec) 第二個參數為 TypeSpec ,前面提到過 TypeSpec 是JavaPoet提供的用來生成類的接口,打開 createType(int sdk) ,霍霍,原來控制將要生成的代碼的邏輯在這里:
private TypeSpec createType(int sdk) {
// 生成類名為bindingClassName的類
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
//ButterKnife的BindingSet初始化都是通過BindingSet的build方法初始化的,所以isFinal一般被初始化為false
if (isFinal) {
result.addModifiers(FINAL);
}
if (parentBinding != null) {
//如果有父類的話,那么注入該子類的時候,也會順帶注入其父類
result.superclass(parentBinding.bindingClassName);
} else {
//如果沒有父類,那么實現Unbinder接口(所以所有生成的輔助類都會繼承Unbinder接口)
result.addSuperinterface(UNBINDER);
}
//增加一個變量名為target,類型為targetTypeName的成員變量
if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}
if (!constructorNeedsView()) {
// Add a delegating constructor with a target type + view signature for reflective use.
result.addMethod(createBindingViewDelegateConstructor(targetTypeName));
}
//核心方法,生成***_ViewBinding方法,我們控件的綁定比如findViewById之類的方法都在這里生成
result.addMethod(createBindingConstructor(targetTypeName, sdk));
if (hasViewBindings() || parentBinding == null) {
//生成unBind方法
result.addMethod(createBindingUnbindMethod(result, targetTypeName));
}
return result.build();
}
接下來讓我們看看核心語句 createBindingConstructor 在 * _ViewBinding方法內到底干了什么:
private MethodSpec createBindingConstructor(TypeName targetType, int sdk) {
//方法修飾符為PUBLIC,并且添加注解為UiThread
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC);
if (hasMethodBindings()) {
//如果有OnClick注解,那么添加方法參數為targetType final target
constructor.addParameter(targetType, "target", FINAL);
} else {
//如果沒有OnClick注解,那么添加方法參數為targetType target
constructor.addParameter(targetType, "target");
}
if (constructorNeedsView()) {
//如果有注解的View控件,那么添加View source參數
constructor.addParameter(VIEW, "source");
} else {
//否則添加Context source參數
constructor.addParameter(CONTEXT, "context");
}
if (hasUnqualifiedResourceBindings()) {
constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "ResourceType")
.build());
}
//如果有父類,那么會根據不同情況調用不同的super語句
if (parentBinding != null) {
if (parentBinding.constructorNeedsView()) {
constructor.addStatement("super(target, source)");
} else if (constructorNeedsView()) {
constructor.addStatement("super(target, source.getContext())");
} else {
constructor.addStatement("super(target, context)");
}
constructor.addCode("\n");
}
//如果有綁定過Field(不一定是View),那么添加this.target = target語句
if (hasTargetField()) {
constructor.addStatement("this.target = target");
constructor.addCode("\n");
}
if (hasViewBindings()) {
if (hasViewLocal()) {
// Local variable in which all views will be temporarily stored.
constructor.addStatement("$T view", VIEW);
}
for (ViewBindings bindings : viewBindings) {
//View綁定的最常用,也是最關鍵的語句,生成類似于findViewById之類的代碼
addViewBindings(constructor, bindings);
}
/**
* 如果將多個view組成一個List或數組,然后進行綁定,
* 比如@BindView({ R.id.first_name, R.id.middle_name, R.id.last_name })
* List<EditText> nameViews;會走這段邏輯
*/
for (FieldCollectionViewBinding binding : collectionBindings) {
constructor.addStatement("$L", binding.render());
}
if (!resourceBindings.isEmpty()) {
constructor.addCode("\n");
}
}
---省略一些綁定resource資源的代碼---
}
addViewBindings 我們簡單看看就好。需要注意的是:
-
因為生成代碼時確實要根據不同條件來生成不同代碼,所以使用了 CodeBlock.Builder 接口。 CodeBlock.Builder 也是JavaPoet提供的,該接口提供了類似字符串拼接的能力
-
生成了類似于 target.fieldBinding.getName() = .findViewById(bindings.getId().code) 或者 target.fieldBinding.getName() = .findRequiredView(bindings.getId().code) 之類的代碼,我們可以清楚的看到,這里沒有用到反射,所以被@BindView注解的變量的修飾符不能為private。
private void addViewBindings(MethodSpec.Builder result, ViewBindings bindings) { if (bindings.isSingleFieldBinding()) { // Optimize the common case where there's a single binding directly to a field. FieldViewBinding fieldBinding = bindings.getFieldBinding(); /** * 這里使用了CodeBlock接口,顧名思義,該接口提供了類似字符串拼接的接口 * 另外,從target.$L 這條語句來看,我們就知道為什么使用BindView注解的 * 變量不能為private了 */ CodeBlock.Builder builder = CodeBlock.builder() .add("target.$L = ", fieldBinding.getName()); boolean requiresCast = requiresCast(fieldBinding.getType()); if (!requiresCast && !fieldBinding.isRequired()) { builder.add("source.findViewById($L)", bindings.getId().code); } else { builder.add("$T.find", UTILS); builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView"); if (requiresCast) { builder.add("AsType"); } builder.add("(source, $L", bindings.getId().code); if (fieldBinding.isRequired() || requiresCast) { builder.add(", $S", asHumanDescription(singletonList(fieldBinding))); } if (requiresCast) { builder.add(", $T.class", fieldBinding.getRawType()); } builder.add(")"); } result.addStatement("$L", builder.build()); return; } List<ViewBinding> requiredViewBindings = bindings.getRequiredBindings(); if (requiredViewBindings.isEmpty()) { result.addStatement("view = source.findViewById($L)", bindings.getId().code); } else if (!bindings.isBoundToRoot()) { result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS, bindings.getId().code, asHumanDescription(requiredViewBindings)); } addFieldBindings(result, bindings); // 監聽事件綁定 addMethodBindings(result, bindings); }addMethodBindings(result, bindings) 實現了監聽事件的綁定,也通過 MethodSpec.Builder 來生成相應的方法,由于源碼太長,這里就不貼源碼了。
小結: createType 方法到底做了什么?
-
生成類名為 className_ViewBing 的類
-
className_ViewBing 實現了 Unbinder 接口(如果有父類的話,那么會調用父類的構造函數,不需要實現Unbinder接口)
-
根據條件生成 className_ViewBing 構造函數(實現了成員變量、方法的綁定)以及 unbind 方法(解除綁定)等
如果簡單使用 ButterKnife ,比如我們的 MainActivity 長這樣

那么生成的最終 MainActivity_ViewBinding 類的代碼就長下面這樣子,和我們分析源碼時預估的樣子差不多。

需要注意的是, Utils.findRequiredViewAsType 、 Utils.findRequiredView 、 Utils.castView 的區別。其實 Utils.findRequiredViewAsType 就是 Utils.findRequiredView (相當于 findViewById )+ Utils.castView (強制轉型,class類接口)。
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,Class<T> cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}
MainActivity_ViewBinding 類的調用過程就比較簡單了。 MainActivity 一般會調用 ButterKnife.bind(this) 來實現View的依賴注入,這個也是 ButterKnife 和Google親兒子 AndroidAnnotations 的區別: AndroidAnnotations 不需要自己手動調用 ButterKnife.bind(this) 等類似的方法就可以實現View的依賴注入,但是讓人蛋疼的是編譯的時候會生成一個子類,這個子類是使用了 AndroidAnnotations 類后面加了一個 _ ,比如 MainActivity 你就要使用 MainActivity_ 來代替,比如 Activity 的跳轉就必須這樣寫: startActivity(new Intent(this,MyActivity_.class)) ,這兩個開源庫的原理基本差不多,哪種方法比較好看個人喜好去選擇吧。
言歸正傳,輔助類生成后,最終的調用過程一般是 ButterKnife.bind(this) 開始,查看 ButterKnife.bind(this) 源碼,最終會走到 createBinding 以及 findBindingConstructorForClass 這個方法中,源碼如下圖所示,這個方法就是根據你傳入的類名找到對應的輔助類,最終通過調用 constructor.newInstance(target, source) 來實現View以及其他資源的綁定工作。這里需要注意的是在 findBindingConstructorForClass 使用輔助類的時候,其實是有用到反射的,這樣第一次使用的時候會稍微降低程序性能,但是 ButterKnife 會把通過反射生成的實例保存到HashMap中,下一次直接從HashMap中取上次生成的實例,這樣就極大的降低了反射導致的性能問題。當然 ButterKnife.bind 方法還允許傳入其他不同的參數,原理基本差不多,最終都會用到我們生成的輔助類,這里就不贅述了。

執行注解處理器
注解處理器已經有了,比如 ButterKnifeProcessor ,那么怎么執行它呢?這個時候就需要用到 android-apt 這個插件了,使用它有兩個目的:
-
允許配置只在編譯時作為注解處理器的依賴,而不添加到最后的APK或library
-
設置源路徑,使注解處理器生成的代碼能被Android Studio正確的引用
這里把使用 ButterKnife 時 android-apt 的配置作為例子,在工程的 build.gradle 中添加 android-apt 插件
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
在項目的build.gradle中添加
apply plugin: 'android-apt'
android {
...
}
dependencies {
compile 'com.jakewharton:butterknife:8.4.0'
apt 'com.jakewharton:butterknife-compiler:8.4.0'
}
總結
ButterKnife 作為一個被廣泛使用的依賴注入庫,有很多優點:
-
沒有使用反射,而是通過Java Annotation Tool動態生成輔助代碼實現了View的依賴注入,提升了程序的性能
-
提高開發效率,減少代碼量
當然也有一些不太友好的地方:
-
會額外生成新的類和方法數,主要是會加速觸及65535方法數,當然,如果App已經有分dex了可以不用考慮
-
也不是完全沒有用到反射,比如第一次調用 ButterKnife.bind(this) 語句使用輔助類的時候就用到了,會稍微影響程序的性能(但是也僅僅是第一次)
ButterKnife 之旅到這里就結束了,至于要不要在項目中使用大家可以根據實際情況來選擇,畢竟適合自己的才是最好的。
參考文檔
-
[初探JavaPoet]
http://blog.ornithopter.me/post/android/javapoet
-
Generating Java Source Files with JavaPoet
http://www.hascode.com/2015/02/generating-java-source-files-with-javapoet/
-
Java注解處理器
http://www.race604.com/annotation-processing/
-
android-apt
http://www.jianshu.com/p/2494825183c5
-
深入淺出ButterKnife
http://km.oa.com/articles/show/286521?kmref=search&from_page=1&no=1&is_from_iso=1
-
ButterKnife框架原理
https://bxbxbai.github.io/2016/03/12/how-butterknife-works/
-
淺析ButterKnife的實現
http://blog.csdn.net/ta893115871/article/details/52497297
-
深入理解ButterKnife源碼并掌握原理
http://www.bcoder.cn/13581.html
-
ButterKnife源碼剖析
http://blog.csdn.net/chenkai19920410/article/details/51020151
來自:http://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA==&mid=2651232205&idx=1&sn=6c24e6eef2b18f253284b9dd92ec7efb&chksm=f1d9eaaec6ae63b82fd84f72c66d3759c693f164ff578da5dde45d367f168aea0038bc3cc8e8&scene=0#wechat_redirect