# 概述
* 編寫代碼時對目標元素進行注解
* 編譯時注解處理器會掃描注解,并通過 JavaPoet 自動生成 Java 文件
* 調用 ButterKnife.bind()方法時,會通過反射拿到之前為目標類生成的 TargetClassName_ViewBinding 類,并調用其構造方法,完成綁定
# 具體分析
## 源碼組成
ButterKnife 的源碼主要的組成部分包括三個:butterknife、butterknife-annotations、butterknife-compiler。其中 butterknife-annotations 聲明了所有的注解;butterknife-compiler 為注解處理器,在編譯階段對注解進行處理;butterknife 則是我們調用的接口。接下來我們一一來看。
## butterknife-annotations
ButterKnife 提供了如下注解:
* 綁定類型的注解:BindAnim、BindArray、BindBitmap、BindBool、BindColor、BindDimen、BindDrawable、BindFloat、BindFont、BindInt、BindString、BindView、BindViews
* 監聽類型注解:OnCheckedChanged、OnClick、OnEditorAction、OnFocusChange、OnItemClick、OnItemLongClick、OnItemSelected、OnLongClick、OnPageChange、OnTextChanged、OnTouch、Optional
## butterknife-compiler
復習下在 EventBus 源碼分析時用到的注解處理器:
注解處理器用來在編譯時掃描和處理注解,會自動生成 .java 文件。
每一個注解處理器都繼承于 AbstractProcessor,
```java
package com.example;
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
```
其中必需的幾個方法如下:
* init(ProcessingEnvironment env):會被外部的工具調用,傳入 ProcessingEnvironment 參數,我們可以從其中獲取到一些工具類
* process():在這里掃描、處理注解以及生成代碼
* getSupportedAnnotationsTypes():在這里定義本注解處理器會注冊到哪些注解上
* getSupportedSourceVersion():指定 Java 版本
那么首先來看看在 butterknife-compiler 中的關鍵類:ButterknifeProcessor。
```java
public final class ButterKnifeProcessor extends AbstractProcessor {
// 初始化工具
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
...
elementUtils = env.getElementUtils();
typeUtils = env.getTypeUtils();
filer = env.getFiler();
}
// 定義所有注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
// 在這里看到了前面聲明的所有注解
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindColor.class);
annotations.add(BindView.class);
...
annotations.addAll(LISTENERS);
return annotations;
}
// 對注解進行處理
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {}
}
```
### process 方法
接下來我們重點看看 process 方法,也就是注解處理器是如何對注解進行操作的。
```java
// 對注解進行處理
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// 掃描注解并解析,得到 Map
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
// 生成 Java 文件
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
```
通過參數 RoundEmviroment 我們可以拿到所有包含特定注解的被注解元素,生成一個 Key 為 TypeElement,Value 為 BindingSet 的 Map;接著遍歷 Map 生成相應的 Java 文件。
先來看看掃描注解并解析的源碼:
```java
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
...
// Process each @BindAnim element.
// 遍歷所有被注解的元素
for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
try {
parseResourceAnimation(element, builderMap, erasedTargetNames);
} catch (Exception e) {
...
}
}
...
// Process each annotation that corresponds to a listener.
for (Class<? extends Annotation> listener : LISTENERS) {
findAndParseListener(env, listener, builderMap, erasedTargetNames);
}
// 把之前的 builderMap 轉換為 bindingMap
...
while (!entries.isEmpty()) {
Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
TypeElement parentType = findParentType(type, erasedTargetNames);
if (parentType == null) {
bindingMap.put(type, builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}
return bindingMap;
}
```
中間省略了一些注解的處理,每種注解處理方式基本一致,以 BindAnim 為例,其中的關鍵方法為 parseResourceAnimation,其他注解的該方法分別對應著(parseBindView、parseResourceString 等)
```java
private void parseResourceAnimation(Element element,
...
// Assemble information on the field.
BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
builder.addResource(new FieldAnimationBinding(getId(qualifiedId), name));
erasedTargetNames.add(enclosingElement);
}
```
parseResourceAnimation 的作用為生成被注解元素所對應類的 Builder,并得到 builderMap。
接下來看看生成 Java 文件的代碼:
```java
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
```
這里使用 JavaPoet 生成了代碼。
源碼分析未完待續!
來看看 JavaPoet 為我們生成的 Java 文件(示例中 MainActivity 中有兩個點擊事件綁定):
```java
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view2131230756;
private View view2131230758;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
view = Utils.findRequiredView(source, R.id.btn_alipay, "method 'onViewClicked'");
view2131230756 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onViewClicked(p0);
}
});
view = Utils.findRequiredView(source, R.id.btn_custom_view, "method 'onViewClicked'");
view2131230758 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onViewClicked(p0);
}
});
}
@Override
@CallSuper
public void unbind() {
if (target == null) throw new IllegalStateException("Bindings already cleared.");
target = null;
view2131230756.setOnClickListener(null);
view2131230756 = null;
view2131230758.setOnClickListener(null);
view2131230758 = null;
}
}
```
在構造方法中,可以看到我們熟悉的點擊事件,View 的綁定同理。
## butterknife
在 ButterKnife 的標準用法中,會在 Activity 或 Fragment 的 onCreate 方法中調用 `ButterKnife.bind(this);` 進行綁定,我們就首先從 bind 方法入手。
```java
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
...
}
}
```
可以看到,通過反射首先拿到 target 的類名,然后找到其對應的構造器,接著創建相應的對象。其中的 findBindingConstructorForClass 方法源碼如下:
```java
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
return bindingCtor;
}
String clsName = cls.getName();
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} catch (ClassNotFoundException e) {
} catch (NoSuchMethodException e) {
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
```
可以看到,根據類名拿到 targetClassName_ViewBinding 類的構造器,并調用其構造方法,進行綁定
# 參考文檔
[ButterKnife 源碼分析](https://www.jianshu.com/p/1c449c1b0fa2)
- 導讀
- Java知識
- Java基本程序設計結構
- 【基礎知識】Java基礎
- 【源碼分析】Okio
- 【源碼分析】深入理解i++和++i
- 【專題分析】JVM與GC
- 【面試清單】Java基本程序設計結構
- 對象與類
- 【基礎知識】對象與類
- 【專題分析】Java類加載過程
- 【面試清單】對象與類
- 泛型
- 【基礎知識】泛型
- 【面試清單】泛型
- 集合
- 【基礎知識】集合
- 【源碼分析】SparseArray
- 【面試清單】集合
- 多線程
- 【基礎知識】多線程
- 【源碼分析】ThreadPoolExecutor源碼分析
- 【專題分析】volatile關鍵字
- 【面試清單】多線程
- Java新特性
- 【專題分析】Lambda表達式
- 【專題分析】注解
- 【面試清單】Java新特性
- Effective Java筆記
- Android知識
- Activity
- 【基礎知識】Activity
- 【專題分析】運行時權限
- 【專題分析】使用Intent打開三方應用
- 【源碼分析】Activity的工作過程
- 【面試清單】Activity
- 架構組件
- 【專題分析】MVC、MVP與MVVM
- 【專題分析】數據綁定
- 【面試清單】架構組件
- 界面
- 【專題分析】自定義View
- 【專題分析】ImageView的ScaleType屬性
- 【專題分析】ConstraintLayout 使用
- 【專題分析】搞懂點九圖
- 【專題分析】Adapter
- 【源碼分析】LayoutInflater
- 【源碼分析】ViewStub
- 【源碼分析】View三大流程
- 【源碼分析】觸摸事件分發機制
- 【源碼分析】按鍵事件分發機制
- 【源碼分析】Android窗口機制
- 【面試清單】界面
- 動畫和過渡
- 【基礎知識】動畫和過渡
- 【面試清單】動畫和過渡
- 圖片和圖形
- 【專題分析】圖片加載
- 【面試清單】圖片和圖形
- 后臺任務
- 應用數據和文件
- 基于網絡的內容
- 多線程與多進程
- 【基礎知識】多線程與多進程
- 【源碼分析】Handler
- 【源碼分析】AsyncTask
- 【專題分析】Service
- 【源碼分析】Parcelable
- 【專題分析】Binder
- 【源碼分析】Messenger
- 【面試清單】多線程與多進程
- 應用優化
- 【專題分析】布局優化
- 【專題分析】繪制優化
- 【專題分析】內存優化
- 【專題分析】啟動優化
- 【專題分析】電池優化
- 【專題分析】包大小優化
- 【面試清單】應用優化
- Android新特性
- 【專題分析】狀態欄、ActionBar和導航欄
- 【專題分析】應用圖標、通知欄適配
- 【專題分析】Android新版本重要變更
- 【專題分析】唯一標識符的最佳做法
- 開源庫源碼分析
- 【源碼分析】BaseRecyclerViewAdapterHelper
- 【源碼分析】ButterKnife
- 【源碼分析】Dagger2
- 【源碼分析】EventBus3(一)
- 【源碼分析】EventBus3(二)
- 【源碼分析】Glide
- 【源碼分析】OkHttp
- 【源碼分析】Retrofit
- 其他知識
- Flutter
- 原生開發與跨平臺開發
- 整體歸納
- 狀態及狀態管理
- 零碎知識點
- 添加Flutter到現有應用
- Git知識
- Git命令
- .gitignore文件
- 設計模式
- 創建型模式
- 結構型模式
- 行為型模式
- RxJava
- 基礎
- Linux知識
- 環境變量
- Linux命令
- ADB命令
- 算法
- 常見數據結構及實現
- 數組
- 排序算法
- 鏈表
- 二叉樹
- 棧和隊列
- 算法時間復雜度
- 常見算法思想
- 其他技術
- 正則表達式
- 編碼格式
- HTTP與HTTPS
- 【面試清單】其他知識
- 開發歸納
- Android零碎問題
- 其他零碎問題
- 開發思路