ButterKnife原始碼分析和手寫
大學剛出來實習那會,自己寫了一個執行時的 ViewById 和 OnClick 註解,用來解決 findViewById 和 setOnClickListener 。其實也是參考 xUtils 的原始碼,後來加了很多的功能擴充套件,如網路檢測等等。關鍵是那時 xUtils 並沒有外掛,自己學著寫了外掛。這個在外包公司能省不少事,技術總監開會說誰誰怎麼怎麼樣,大家要像他學習啊。工資一下漲了不少,裝 B 的技能瞬間提升了一個檔次。隔年回學校拿了畢業證,公司就關門了,這是幹垮的第一家。
後來 ButterKnife 火了,相信很多人都用過,說什麼沒有用反射,而且自帶外掛也不要寫程式碼。我第一反應是蒙的,很好奇不用反射也行?這麼厲害。今天我就帶大家來了解一下 ButterKnife 的原始碼,自己動手去寫,相信這樣會有更深層次的理解,自己手寫不代表我們重複去造輪子。也不是沒事幹,而是知識是用來喚醒智慧的,我們可以把這些知識用到其他地方,比如我們繞過微信支付的侷限性,自動讀取生成配置程式碼等等。從原始碼中汲取營養,這個還是蠻重要。
1. ButterKnife 原理分析
分析原始碼之前我們首先要會使用,這裡就不做詳細介紹,如果不太會的哥們可以去看看別人的文章。網上也有很多分析原始碼的文章,都可以看看,我其實也是看了很多這方面的文章。找到 github 地址,都有教我們怎麼使用。當我們點選執行的時候,仔細找找我們能找到這麼一些程式碼:
// Generated code from Butter Knife. Do not modify!
package com.darren.architect_day05;
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;
target.textView1 = Utils.findRequiredViewAsType(source, R.id.tv1, "field 'textView1'" , TextView.class);
target.textView2 = Utils.findRequiredViewAsType(source, R.id.tv2, "field 'textView2'", TextView.class);
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.textView1 = null;
target.textView2 = null;
}
}
看到這裡我想很多人都應該懂了,其實當我們每次點選執行的時候, 都會去掃描執行時的註解,然後自動生成這麼一個類,是自動生成的不是我們自己寫的。規則大概就像上面這樣,xxx_ViewBinding 作為類名,實現 Unbinder 在 xxx_ViewBinding 的建構函式裡面去 findViewById 或者 setOnclickListener。我們只要在 MainActivity 中去例項化一個 MainActivity_ViewBinding 物件,那麼我們 MainActivity 裡面的所有屬性就都會被賦值了。這個目前來看完全沒有涉及到任何反射,那麼我們只需要去了解怎麼生成程式碼豈不就行了。我們按照這個思路,自己動手來寫一個 ButterKnife 。
2. apt 生成程式碼
怎麼自動生成程式碼,這個其實在 thinking in java 這本書中有提到,這個屬於 mirror 板塊的知識。實在不行嘞,我們可以參考 ButterKnife 的寫法。我們新建一個 Java 工程,注意一定要是一個 Java 工程,否則待會有可能找不到類。
新建 ButterKnifeProcessor 繼承 AbstractProcessor
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
/**
* 用來指定支援的 AnnotationTypes
*
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
/**
* 參考了 ButterKnife 的寫法
*
* @return
*/
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindView.class);
return annotations;
}
/**
* 用來指定支援的 SourceVersion
*
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 除錯列印
System.out.println("------------------------------------>");
System.out.println("------------------------------------>");
return false;
}
}
上面就這麼簡單,點選執行apk,如果你能看到下面這個列印,代表配置這方面是沒有問題的,如果沒有看到任何列印,說明沒起作用,要先找找問題:
如果我們在程式碼的任何地方有註解,都會來到 process 這個方法裡面,我們只要在這個方法裡面生成程式碼就可以了,至於註解反射泛型的基礎知識這個之前講過,這裡就不做詳細的介紹。
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 除錯列印
// System.out.println("------------------------------------>");
// System.out.println("------------------------------------>");
Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
// 解析 Element
Map<Element, List<Element>> analysisElementMap = new LinkedHashMap<>();
for (Element bindViewElement : bindViewElements) {
Element enclosingElement = bindViewElement.getEnclosingElement();
List<Element> elements = analysisElementMap.get(enclosingElement);
if (elements == null) {
elements = new ArrayList<>();
analysisElementMap.put(enclosingElement, elements);
}
elements.add(bindViewElement);
}
// 生成 java 類
for (Map.Entry<Element, List<Element>> entry : analysisElementMap.entrySet()) {
Element enclosingElement = entry.getKey();
List<Element> elements = entry.getValue();
String classNameStr = enclosingElement.getSimpleName().toString();
ClassName unbinderClassName = ClassName.get("com.butterknife", "Unbinder");
// 組裝類: xxx_ViewBinding implements Unbinder
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(classNameStr + "_ViewBinding")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addSuperinterface(unbinderClassName);
ClassName callSuperClassName = ClassName.get("android.support.annotation", "CallSuper");
// 組裝unbind 方法
MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unbind")
.addAnnotation(Override.class)
.addAnnotation(callSuperClassName)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID);
ClassName uiThreadClassName = ClassName.get("android.support.annotation", "UiThread");
ClassName parameterClassName = ClassName.bestGuess(classNameStr);
// 組裝建構函式: public xxx_ViewBinding(xxx target)
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
.addAnnotation(uiThreadClassName)
.addModifiers(Modifier.PUBLIC)
.addParameter(parameterClassName, "target");
// 新增 target.textView1 = Utils.findViewById(target,R.id.tv1);
for (Element bindViewElement : elements) {
String filedName = bindViewElement.getSimpleName().toString();
ClassName utilClassName = ClassName.get("com.butterknife", "Utils");
int resId = bindViewElement.getAnnotation(BindView.class).value();
constructorBuilder.addStatement("target.$L = $T.findViewById(target,$L)",
filedName, utilClassName, resId);
}
typeSpecBuilder.addMethod(constructorBuilder.build());
typeSpecBuilder.addMethod(unbindMethodBuilder.build());
try {
// 寫入生成 java 類
String packageName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
JavaFile.builder(packageName, typeSpecBuilder.build())
.addFileComment("ButterKnife自動生成")
.build().writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
System.out.println("翻車了");
}
}
return false;
}
最後我們看下生成的類檔案,下一篇將利用編譯時註解的知識點,生成程式碼,解決掉我們開發過程中的一些棘手問題。
視訊講解地址:週六晚八點