文章目錄
一. 概述
ButterKnife 是一個依賴注入框架,主要用於綁定View、一些View的事件等等,可以大大減少findViewById以及設置View事件監聽器的代碼,並且框架的性能相比於傳統寫法也沒有什麼太大的損耗。
二. 簡單使用
ButterKnife的用法十分簡單
導入依賴
android {
...
// Butterknife requires Java 8.
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'com.jakewharton:butterknife:10.2.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
}
在代碼中使用
在需要注入值的屬性或者方法上使用對應的註解修飾,然後在onCreate中啓動註解處理工具,然後在onDestroy中移除對屬性的綁定。
public class MainActivity extends AppCompatActivity {
@BindView(R.id.cl)
ConstraintLayout cl;
@BindView(R.id.bt)
Button bt;
@BindString(R.string.app_name)
String appName;
@BindDrawable(R.drawable.ic_launcher_background)
Drawable icon;
@BindColor(R.color.colorAccent)
int color;
private Unbinder mUnbinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUnbinder = ButterKnife.bind(this);
}
@OnClick(R.id.bt)
void onClick() {
}
@Override
protected void onDestroy() {
super.onDestroy();
mUnbinder.unbind();
}
}
三. 源碼分析
我們將使用上方的例子來講解ButterKnife,我們先來看看我們在例子中做了哪些操作
- 通過BindView將佈局中對應id的佈局綁定到對應的View
- 通過BindString將一個資源字符串綁定到一個String類型的屬性
- 通過BindDrawable將一個資源類型的圖片綁定到一個Drawable的屬性
- 通過BindColor將一個顏色綁定到一個int類型的屬性
- 通過OnClick爲R.id.bt對用的控件設置了點擊事件的方法回調
public class MainActivity extends AppCompatActivity {
@BindView(R.id.cl)
ConstraintLayout cl;
@BindView(R.id.bt)
Button bt;
@BindString(R.string.app_name)
String appName;
@BindDrawable(R.drawable.ic_launcher_background)
Drawable icon;
@BindColor(R.color.colorAccent)
int color;
private Unbinder mUnbinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUnbinder = ButterKnife.bind(this);
}
@OnClick(R.id.bt)
void onClick() {
}
@Override
protected void onDestroy() {
super.onDestroy();
mUnbinder.unbind();
}
}
3.1 ViewBinding 類
上述代碼使用了ButterKnife註解,在編譯時就會被系統掃描到,然後在同級的包名目錄下會生成一個[類名]_ViewBinding的一個類。
現在我們來看生成的MainActivity_ViewBinding類,這個類代碼十分簡單。
- 屬性: 有一個MainActivity的屬性,用於持有對MainActivity對象的引用。
- 構造方法:兩個構造方法,在構造方法內對MainActivity中使用註解修飾的屬性進行賦值或爲指定的控件設置監聽器
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view7f070042;
@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;
// 找到id爲R.id.cl的控件,然後將其賦值給MainActivity中的名爲cl的屬性,也就是使用BindView修飾,註解值爲R.id.cl的控件
target.cl = Utils.findRequiredViewAsType(source, R.id.cl, "field 'cl'", ConstraintLayout.class);
// 同理 找到id爲.id.bt的控件,將其賦值給MainActivity中的名爲bt的屬性
view = Utils.findRequiredView(source, R.id.bt, "field 'bt' and method 'onClick'");
target.bt = Utils.castView(view, R.id.bt, "field 'bt'", Button.class);
view7f070042 = view;
//爲R.id.bt的控件設置監聽器
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onClick();
}
});
Context context = source.getContext();
Resources res = context.getResources();
// 將R.color.colorAccent對應的顏色賦值給MainActivity中名爲color的屬性
target.color = ContextCompat.getColor(context, R.color.colorAccent);
// 將R.drawable.ic_launcher_background對應的圖片賦值給MainActivity中名爲icon的屬性
target.icon = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background);
// 將R.string.app_name 對應的字符串值賦值給MainActivity中名爲appName的屬性
target.appName = res.getString(R.string.app_name);
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
// 移除被BindView修飾的View控件的引用
target.cl = null;
target.bt = null;
// 移除監聽器
view7f070042.setOnClickListener(null);
view7f070042 = null;
}
}
這裏以id爲R.id.bt的控件爲例進行講解:
可以看到這邊通過Utils這個工具類去獲取這個id的控件,findRequiredView有三個參數,第一個source爲DecorView對象,是MainActivity的頂級容器,第二個參數是需要獲取控件的id,第三個參數爲描述信息
public MainActivity_ViewBinding(final MainActivity target, View source) {
...
view = Utils.findRequiredView(source, R.id.bt, "field 'bt' and method 'onClick'");
}
我們來看看Utils工具類,不難發現,最終還是通過findViewById從source(這裏是MainActivity的頂層View)獲取到了控件,然後將其返回。
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);
}
public static View findRequiredView(View source, @IdRes int id, String who) {
//可以看到最終還是通過findViewById從source中找到View
View view = source.findViewById(id);
if (view != null) {
return view;
}
...
}
我們再回到MainActivity_ViewBinding的構造方法中,由於在例子中我們在屬性名爲bt的按鈕設置了BindView的註解,所以這裏將找到的控件賦值給MainActivity的bt屬性,這裏由於是直接賦值,因此被註解修飾的屬性訪問權限不能爲private。
由於在例子中我們還給R.id.bt的控件設置了點擊事件的回調方法,所以這邊找到R.id.bt的控件之後,還需爲其設置點擊事件,並在回調方法中回調MainActivity的onClick方法。因此bt對應的控件被點擊,MainActivity的onClick會被回調。
public MainActivity_ViewBinding(final MainActivity target, View source) {
...
//獲取id爲R.id.bt的View
view = Utils.findRequiredView(source, R.id.bt, "field 'bt' and method 'onClick'");
target.bt = Utils.castView(view, R.id.bt, "field 'bt'", Button.class);
view7f070042 = view;
//爲R.id.bt的控件設置監聽器
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onClick();
}
});
}
最後再講下unbind這個方法,這個方法主要是用於移除之前綁定控件或者監聽器的引用,在Activity銷燬前調用,用於及時回收不必要的內存。
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
// 移除被BindView修飾的View控件的引用
target.cl = null;
target.bt = null;
// 移除監聽器
view7f070042.setOnClickListener(null);
view7f070042 = null;
}
好了,MainActivity_ViewBinding類算是講解完了,這裏做一個小結
- Activity或者Fragment中有使用BindView等註解,在項目編譯時,ButterKnife會在同級包目錄下生成[類型]_ViewBinding類(一個類對應一個ViewBinding類)
- 在生成的類的構造方法中會對目標類中所有被註解修飾的屬性進行值的注入,若存在監聽器相關的註解,則同時爲控件創建事件監聽器,並回調目標類對應的方法
3.2 ButterKnife.bind
看到這,相信大家心裏都有疑問,雖然生成了這樣一個類,它在構造方法中完成了值的注入,但是也沒看見ButterKnife調用啊。
我們再回到例子,看看是不是還漏了什麼東西。對的,想必你也發現了,我們在onCreate方法中將MainActivity對象注入到了ButterKnife中,我們來看看這裏做了什麼操作。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUnbinder = ButterKnife.bind(this);
}
}
ButterKnife這個類的代碼也比較少
public final class ButterKnife {
private ButterKnife() {
throw new AssertionError("No instances.");
}
private static final String TAG = "ButterKnife";
private static boolean debug = false;
// 用於緩存_ViewBinding類的構造方法,避免每次通過反射去獲取
@VisibleForTesting
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
/** Control whether debug logging is enabled. */
public static void setDebug(boolean debug) {
ButterKnife.debug = debug;
}
// Activity注入
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}
// View注入,例如自定義View中使用註解
@NonNull @UiThread
public static Unbinder bind(@NonNull View target) {
return bind(target, target);
}
// 對話框注入
@NonNull @UiThread
public static Unbinder bind(@NonNull Dialog target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}
// Activity注入
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull Activity source) {
View sourceView = source.getWindow().getDecorView();
return bind(target, sourceView);
}
// 對話框注入
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull Dialog source) {
View sourceView = source.getWindow().getDecorView();
return bind(target, sourceView);
}
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);
} catch (Exception e) {
...
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
// 尋找_ViewBinding類的構造方法
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
// 緩存中存在該構造方法直接返回
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null || BINDINGS.containsKey(cls)) {
return bindingCtor;
}
// 過濾系統類
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")
|| clsName.startsWith("androidx.")) {
return null;
}
try {
// 加載ViewBinding類到內存
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
// 獲取ViewBinding類的構造方法
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} catch (Exception e) {
...
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
// 獲取成功後把狗雜方法進行緩存
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
}
這裏繼續使用上述的例子進行講解
在MainActivity的onCreate中調用了bind方法,將Activity注入到ButterKnife,可以看到最終調用了兩個參數的bind方法,這邊target就是MainActivity對象,source爲MainActivity的DecorView。
可以看到在bind方法中根據targetClass(這裏是指MainActivity)去尋找_ViewBinding類的構造方法
// Activity注入
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
// 得到頂層容器DecorView
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
// 尋找_ViewBinding的構造方法
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
try {
return constructor.newInstance(target, source);
} catch (Exception e) {
...
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
接下來來看看findBindingConstructorForClass方法的實現
- 首先根據targetClass去緩存中尋找是否存在targetClass對用的_ViewBinding類的構造方法,緩存中存在就直接返回
- 緩存中不存在,過濾一些系統的類後,根據targetClass的得到對應 _ViewBinding的全類名,將該_ViewBinding類通過ClassLoader加載到內存,然後通過Class去獲取類對應的構造方法
- 獲取到構造方法之後將構造方法加入到緩存Map中,然後返回該構造方法
// 尋找_ViewBinding類的構造方法
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
// 緩存中存在該構造方法直接返回
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null || BINDINGS.containsKey(cls)) {
return bindingCtor;
}
// 過濾系統類
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")
|| clsName.startsWith("androidx.")) {
return null;
}
try {
// 加載ViewBinding類到內存
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
// 獲取ViewBinding類的構造方法
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} catch (Exception e) {
...
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
// 獲取成功後把構造方法進行緩存
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
現在再回到bind方法中,通過findBindingConstructorForClass方法拿到了ViewBinding類的構造方法,然後通過構造方法創建了一個ViewBinding的實例(這裏是MainActivity_ViewBinding類的實例)。
我們在前一個小結中已經提到,MainActivity中的值注入和監聽器的綁定是在MainActivity_ViewBinding的構造方法中完成的,而這個MainActivity_ViewBinding實例就是在ButterKnife.bind方法被調用時創建的,此時整個依賴注入流程就已經完成了。
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);
} catch (Exception e) {
...
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
小結: 通過ButterKnife的bind方法生成了一個ViewBinding類的實例,然後在ViewBinding類中進行了被註解修飾屬性值的注入以及事件監聽回調方法的綁定。
3.3 註解處理器
是不是恍然大悟,原來ButterKnife幫我們做了這些事情啊,其實ButterKnife幫我們做的事情遠不止這些,前面我們提到只要你在Activity或者Fragment等地方正確的使用了ButterKnife的註解,在編譯時ButterKnife就會爲這些使用了註解的Activity或者Fragment生成對應的ViewBinding類,那麼這種操作是如何實現的呢,這就要用到APT(Annotation Processing Tool)技術了。
不瞭解APT的同學可以戳這裏 APT入門
3.3.1 整體流程
註解處理器在編譯時可以獲取到註解的相關信息,例如被註解修飾的類元素,字段元素等等,它在ButterKnife中的作用就是通過根據註解的相關信息,在事先定義好的模板類中填充代碼,最終將這個類信息輸出到文件中。
首先看一下註解處理的核心類 ButterKnifeProcessor,該類在編譯時會被系統掃描到,從而執行註解處理的代碼。
支持的註解類型
//支持的監聽器註解類型
private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(//
OnCheckedChanged.class, //
OnClick.class, //
OnEditorAction.class, //
OnFocusChange.class, //
OnItemClick.class, //
OnItemLongClick.class, //
OnItemSelected.class, //
OnLongClick.class, //
OnPageChange.class, //
OnTextChanged.class, //
OnTouch.class //
);
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindAnim.class);
annotations.add(BindArray.class);
annotations.add(BindBitmap.class);
annotations.add(BindBool.class);
annotations.add(BindColor.class);
annotations.add(BindDimen.class);
annotations.add(BindDrawable.class);
annotations.add(BindFloat.class);
annotations.add(BindFont.class);
annotations.add(BindInt.class);
annotations.add(BindString.class);
annotations.add(BindView.class);
annotations.add(BindViews.class);
annotations.addAll(LISTENERS);
return annotations;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
process方法爲註解處理的核心方法,該方法主要做了兩個事情
- 尋找並且解析被註解修飾的元素,返回一個類元素與它對應Viewbinding類信息的一個Map
- 遍歷所有的ViewBinding類信息類,爲每個被註解修飾的類元素創建一個ViewBinding類
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//尋找並且解析被註解修飾的元素,返回一個類元素與它對應viewbinding類信息的一個Map
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
//生成被註解修飾類的viewbinding類Java代碼
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;
}
3.3.2 註解編譯器項目結構
在分析細節前,首先看一下ButterKnife註解編譯器的項目結構
每個類或接口的作用:
類/接口 | 作用 |
---|---|
BindingSet | ViewBinding類內部所有的信息(屬性,方法等),一個類對應一個BindingSet,同時具有生成模板代碼的能力 |
ButterKnifeProcessor | 註解處理器 |
FieldAnimationBinding | 被註解修飾的動畫屬性信息 |
FieldCollectionViewBinding | 被BindViews修飾的控件組屬性信息 |
FieldDrawableBinding | 被註解修飾的Drawable屬性信息(drawbable對應的id,屬性的名稱等) |
FieldResourceBinding | 被註解修飾的資源屬性信息(這裏的資源包括bitmap,dimen,color,string等) |
FieldTypefaceBinding | 被註解修飾的字體屬性的信息(屬性的名字,字體的id,字體的風格) |
FieldViewBinding | 被註解修飾的控件屬性的信息(控件名、類型、是否必須) |
Id | 資源id信息封裝,包括id的值、id的代碼 |
MemberViewBinding | 被註解修飾成員信息接口(成員包括屬性和方法) |
MethodViewBinding | 被註解修飾方法的信息(方法名、方法的參數、返回值) |
Parameter | 監聽器方法形參的封裝(形參的類型,形參的位置) |
ResourceBinding | 資源綁定信息接口 |
ViewBinding | 控件綁定信息,存儲了被BindView修飾控件的信息以及該控件的事件監聽信息,一個View對應一個ViewBinding |
從上可知 ButterKnife註解編譯器中的類可以分爲三類
-
註解處理器
-
用於保存被註解修飾屬性或者方法的信息描述類
-
BindingSet 存儲了一個ViewBinding類所有元素信息,並且提供生成模板代碼的能力
3.3.3 process方法
1. 解析註解獲得ViewBinding類信息集合
我們現在開始分析process方法的細節,在process中首先調用了findAndParseTargets方法,去解析所有被ButterKnife修飾的註解,最終返回一個Map,Map的Key存儲被ButterKnife註解修飾成員的類,value保存了ViewBinding類相關信息。
//尋找並且解析被註解修飾的元素,返回一個類元素與它對應viewbinding類信息的一個Map
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
進入findAndParseTargets方法,這個方法主要有兩個作用:
- 解析各種ButterKnife註解
- 爲每一個被註解修飾成員元素的父元素(類元素)生成對應的ViewBinding類結構信息(BindingSet)
這邊先從解析註解開始,以解析BindView註解爲例
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
// 用於保存被註解修飾成員元素的父元素(這裏是類元素),ButterKnife註解是用於修飾字段或者方法的
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
// ... 省略處理其他處理註解的代碼
// 遍歷所有被BindView修飾的元素
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
try {
// 解析該元素以及註解
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
...
return bindingMap;
}
parseBindView方法主要是用於解析BindView註解,得到被註解修飾屬性的信息,然後將其保存起來
執行流程:
-
檢查註解修飾的合法性(包括被註解修飾屬性的訪問權限以及註解修飾的類合法性)
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) { //得到當前字段元素的父元素,也就是類元素 TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); //判斷元素的訪問權限的合法性以及是否判斷在系統的api類中使用了ButterKnife註解 boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element) || isBindingInWrongPackage(BindView.class, element);private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) { // 如果註解修飾的是泛型 TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.TYPEVAR) { /** * 找到泛型類型的上界 例如 * @BindView(R.id.xx) * T view; * 其中T的類型爲 ? extend View */ TypeVariable typeVariable = (TypeVariable) elementType; elementType = typeVariable.getUpperBound(); } // 得到類元素的全名 Name qualifiedName = enclosingElement.getQualifiedName(); // 得到屬性的名字 Name simpleName = element.getSimpleName(); if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) { // 若當前類不是View類的子類並且類型元素不是接口 if (elementType.getKind() == TypeKind.ERROR) { note(element, "@%s field with unresolved type (%s) " + "must elsewhere be generated as a View or interface. (%s.%s)", BindView.class.getSimpleName(), elementType, qualifiedName, simpleName); } else { error(element, "@%s fields must extend from View or be an interface. (%s.%s)", BindView.class.getSimpleName(), qualifiedName, simpleName); hasError = true; } } // 存在錯誤則直接返回 if (hasError) { return; } .... }
訪問權限檢查: 屬性或者方法的訪問權限不能爲private或者static
// 判斷訪問權限是否合法 private boolean isInaccessibleViaGeneratedCode(Class<? extends Annotation> annotationClass, String targetThing, Element element) { boolean hasError = false; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); //屬性或者方法的訪問權限不能是private或者static Set<Modifier> modifiers = element.getModifiers(); if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) { error(element, "@%s %s must not be private or static. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // 元素的父元素必須爲類 if (enclosingElement.getKind() != CLASS) { error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // 類元素不能爲private if (enclosingElement.getModifiers().contains(PRIVATE)) { error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } return hasError; }
註解修飾屬性對應的類合法性: 被註解修飾的屬性對應的類不能爲系統類
// 是否綁定的類不合法,無法綁定系統類 private boolean isBindingInWrongPackage(Class<? extends Annotation> annotationClass, Element element) { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); String qualifiedName = enclosingElement.getQualifiedName().toString(); //只要被註解修飾屬性對應的類爲系統相關類則爲不合法 if (qualifiedName.startsWith("android.")) { error(element, "@%s-annotated class incorrectly in Android framework package. (%s)", annotationClass.getSimpleName(), qualifiedName); return true; } if (qualifiedName.startsWith("java.")) { error(element, "@%s-annotated class incorrectly in Java framework package. (%s)", annotationClass.getSimpleName(), qualifiedName); return true; } return false; }
註解修飾的屬性類型合法性:被BindView修飾的元素需要是View的子類或者接口
// 得到類元素的全名 Name qualifiedName = enclosingElement.getQualifiedName(); // 得到屬性的名字 Name simpleName = element.getSimpleName(); if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) { // 若當前類不是View類的子類並且類型元素不是接口 if (elementType.getKind() == TypeKind.ERROR) { note(element, "@%s field with unresolved type (%s) " + "must elsewhere be generated as a View or interface. (%s.%s)", BindView.class.getSimpleName(), elementType, qualifiedName, simpleName); } else { error(element, "@%s fields must extend from View or be an interface. (%s.%s)", BindView.class.getSimpleName(), qualifiedName, simpleName); hasError = true; } }
-
將被註解的屬性信息保存
對註解的合法性檢查完之後,就是要取出註解裏的佈局id以及對應屬性的信息,將其保存起來
- 在緩存中尋找當前屬性對應的類是否已經存在,若存在則校驗對應的id是否重複綁定;若不存在,創建當前屬性對應類的BindingSet
- 將佈局id以及被註解修飾屬性的信息包裝成FieldViewBinding,並將該包裝類加入到BindingSet中
- 將被註解修飾屬性的父元素(也就是類元素)保存起來
// 得到BindView內的id值
int id = element.getAnnotation(BindView.class).value();
//在緩存中取出BindingSet
BindingSet.Builder builder = builderMap.get(enclosingElement);
// 將id值封裝成Id對象
Id resourceId = elementToId(element, BindView.class, id);
if (builder != null) {
//已經存在類信息包裝類
String existingBindingName = builder.findExistingBindingName(resourceId);
if (existingBindingName != null) {
// 該資源id已經被綁定過了
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
BindView.class.getSimpleName(), id, existingBindingName,
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
} else {
//不存在則創建
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
String name = simpleName.toString();
TypeName type = TypeName.get(elementType);
// 判斷字段的是是否能爲空
boolean required = isFieldRequired(element);
// 將字段信息添加到類信息包裝類中
builder.addField(resourceId, new FieldViewBinding(name, type, required));
//將被註解的元素的類元素保存起來
erasedTargetNames.add(enclosingElement);
完整代碼
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
//得到當前字段元素的上級類元素
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
//判斷元素的訪問權限的合法性以及是否判斷在系統的api類中使用了ButterKnife註解
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
|| isBindingInWrongPackage(BindView.class, element);
// 如果註解修飾的是泛型
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
/**
* 找到泛型類型的上界 例如
* @BindView(R.id.xx)
* T view;
* 其中T的類型爲 ? extend View
*/
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
// 得到類元素的全名
Name qualifiedName = enclosingElement.getQualifiedName();
// 得到屬性的名字
Name simpleName = element.getSimpleName();
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
// 若當前類不是View類的子類並且類型元素不是接口
if (elementType.getKind() == TypeKind.ERROR) {
note(element, "@%s field with unresolved type (%s) "
+ "must elsewhere be generated as a View or interface. (%s.%s)",
BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
} else {
error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
BindView.class.getSimpleName(), qualifiedName, simpleName);
hasError = true;
}
}
// 存在錯誤則直接返回
if (hasError) {
return;
}
// 得到BindView內的id值
int id = element.getAnnotation(BindView.class).value();
//在緩存中取出BindingSet
BindingSet.Builder builder = builderMap.get(enclosingElement);
// 將id值封裝成Id對象
Id resourceId = elementToId(element, BindView.class, id);
if (builder != null) {
//已經存在類信息包裝類
String existingBindingName = builder.findExistingBindingName(resourceId);
if (existingBindingName != null) {
//之前已經添加了字段信息
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
BindView.class.getSimpleName(), id, existingBindingName,
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
} else {
//不存在則創建
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
String name = simpleName.toString();
TypeName type = TypeName.get(elementType);
// 判斷字段的是是否能爲空
boolean required = isFieldRequired(element);
// 將字段信息添加到類信息包裝類中
builder.addField(resourceId, new FieldViewBinding(name, type, required));
//將被註解的元素的類元素保存起來
erasedTargetNames.add(enclosingElement);
}
小結:
- 解析BindView註解首先會校驗註解的合法性,依次是權限合法性,類合法性,類型合法性,不合法則返回
- 判斷緩存中是否存在屬性對應類的BindingSet(ViewBinding描述信息),存在則需要判斷id是否被重複綁定,不存在則創建
- 將BindView的值以及對應修飾屬性的信息封裝成FieldViewBinding加入到BindingSet中
解析完所有的註解之後,會得到一個Map,這個Map的Key爲被ButterKnife註解修飾的類元素,Value爲ViewBinding類信息包裝類。在使用ButterKnife時,我們爲了不用每次都書寫ButterKnife.bind方法,通常會把這些代碼放在BaseActivity中,或者在父類中綁定一些公有的屬性,也就是存在父類和子類同時使用ButterKnife註解的情況。因此我們生成的ViewBinding類也會存在一些繼承關係,所以需要爲每一個ViewBinding類確定其繼承關係,找到它可能存在的父類。
- 找到所有存在屬性被註解修飾的父類元素
- 爲每一個元素和存在的父元素建立對應關係,最終得到所有的ViewBinding信息集合
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
...
//解析處理所有ButterKnife註解
// 找到所有存在屬性被註解修飾的父類元素
Map<TypeElement, ClasspathBindingSet> classpathBindings =
findAllSupertypeBindings(builderMap, erasedTargetNames);
//把類信息包裝集合放入隊列中
Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
new ArrayDeque<>(builderMap.entrySet());
// 一個類對應一個類信息包裝類,也就是每一個使用了ButterKnife註解的類對應viewBinding類的信息
Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
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, classpathBindings.keySet());
if (parentType == null) {
//當前類不存在父類使用了ButterKnife註解,直接保存當前類元素和對應類信息包裝類
bindingMap.put(type, builder.build());
} else {
// 存在父類使用了ButterKnife註解
BindingInformationProvider parentBinding = bindingMap.get(parentType);
if (parentBinding == null) {
parentBinding = classpathBindings.get(parentType);
}
if (parentBinding != null) {
//爲當前viewBinding類設置父類信息
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// 存在父類使用了註解的情況,但父類還未被處理,重新將元素加入隊列尾部,延後處理
entries.addLast(entry);
}
}
}
...
return bindingMap;
}
2. 生成Java代碼
在解析處理註解後得到了所有ViewBinding信息集合,現在需要將這些所有的ViewBinding生成具體的Java代碼。
可以看到代碼中遍歷生成的集合,調用BindingSet的brewJava生成Java代碼
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//尋找並且解析被註解修飾的元素,返回一個類元素與它對應viewbinding類信息的一個Map
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
//生成被註解修飾類的viewbinding類Java代碼
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
// 將Java類輸出到文件
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
進入brewJava方法,這裏使用了javapoet框架來生成代碼,createType方法用於生成類信息
JavaFile brewJava(int sdk, boolean debuggable) {
TypeSpec bindingConfiguration = createType(sdk, debuggable);
return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
進入createType方法
執行流程:
- 創建類結構,若存在父類則繼承父類,不存在則實現接口
- 判斷是否需要添加成員屬性target字段,該字段在使用了BindView或者事件相關注解時target會被添加
- 根據注入的類型添加只有target形參的構造方法
- 判斷創建的構造方法中是否需要添加View形參,若沒有,則默認添加一個T_ViewBinding(T target, View source)兩個參數的構造方法
- 添加最終進行依賴注入和事件綁定的構造方法
- 添加unbind方法
private TypeSpec createType(int sdk, boolean debuggable) {
//1. 創建類
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC)
.addOriginatingElement(enclosingElement);
if (isFinal) {
result.addModifiers(FINAL);
}
if (parentBinding != null) {
//存在父類添加了ButterKnife註解,生成ViewBinding類同樣繼承父類對應的ViewBinding
result.superclass(parentBinding.getBindingClassName());
} else {
// 不存在父類,實現Unbinder接口
result.addSuperinterface(UNBINDER);
}
//2. 是否在ViewBinding中添加target屬性,target是注入類的應用,在使用了BindView或者事件相關注解時target會被添加
if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}
//3. 創建只有target形參的構造方法
if (isView) {
result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog());
}
if (!constructorNeedsView()) {
// 4. 如果一個參數的構造方法中不要用到view,創建一個兩個參數的構造方法,一個參數爲target,另一個爲view
// T_ViewBinding(T target, View source),例如類沒有使用BindView和事件相關注解,此時構造方法中不需要用到View,此時會自動創建這個兩個參數的構造方法
result.addMethod(createBindingViewDelegateConstructor());
}
//5. 添加具體綁定屬性,方法的構造方法
result.addMethod(createBindingConstructor(sdk, debuggable));
//6. 添加unbind方法
if (hasViewBindings() || parentBinding == null) {
result.addMethod(createBindingUnbindMethod(result));
}
return result.build();
}
我們這邊着重看下第5個步驟,創建了一個最終的構造方法,其他構造方法都會調用該構造方法。該構造方法是真正進行依賴注入和事件綁定的方法。
執行流程
- 首先添加target形參,若存在事件綁定,則target參數需要用final修飾,因爲事件監聽器以匿名類方法設置
- 根據綁定參數情況選擇第二個參數的類型,若只是資源綁定,則第二個參數爲Context,若存在控件或者方法綁定則第二個參數爲View
- 如果存在父類,則添加調用父類構造方法的代碼
- 若有控件或者方法綁定,target爲成員屬性,則需要給該target屬性賦值
- 存在控件綁定,進行控件值注入以及事件綁定
- 存在資源綁定,則進行資源值注入
private MethodSpec createBindingConstructor(int sdk, boolean debuggable) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC);
//1. 添加target形參
if (hasMethodBindings()) {
constructor.addParameter(targetTypeName, "target", FINAL);
} else {
constructor.addParameter(targetTypeName, "target");
}
//2. 添加第二個形參
if (constructorNeedsView()) {
// 添加View形參
constructor.addParameter(VIEW, "source");
} else {
constructor.addParameter(CONTEXT, "context");
}
if (hasUnqualifiedResourceBindings()) {
// Aapt can change IDs out from underneath us, just suppress since all will work at runtime.
constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "ResourceType")
.build());
}
if (hasOnTouchMethodBindings()) {
constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
.addMember("value", "$S", "ClickableViewAccessibility")
.build());
}
// 3. 是否存在父類
if (parentBinding != null) {
//被註解類的類存在父類頁使用了ButterKnife註解
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");
}
//4.添加target屬性的賦值
if (hasTargetField()) {
constructor.addStatement("this.target = target");
constructor.addCode("\n");
}
// 5. 控件和事件綁定
if (hasViewBindings()) {
if (hasViewLocal()) {
//是否要創建View的局部變量,一般有給控件設置監聽器註解時,會生成該局部變量
constructor.addStatement("$T view", VIEW);
}
// 添加View控件的綁定代碼
for (ViewBinding binding : viewBindings) {
addViewBinding(constructor, binding, debuggable);
}
// 添加使用BindViews綁定控件的代碼
for (FieldCollectionViewBinding binding : collectionBindings) {
constructor.addStatement("$L", binding.render(debuggable));
}
if (!resourceBindings.isEmpty()) {
constructor.addCode("\n");
}
}
// 6. 資源綁定
if (!resourceBindings.isEmpty()) {
if (constructorNeedsView()) {
constructor.addStatement("$T context = source.getContext()", CONTEXT);
}
if (hasResourceBindingsNeedingResource(sdk)) {
constructor.addStatement("$T res = context.getResources()", RESOURCES);
}
for (ResourceBinding binding : resourceBindings) {
constructor.addStatement("$L", binding.render(sdk));
}
}
return constructor.build();
}
這邊同樣看第5個步驟,對控件和事件的綁定,這邊遍歷所有需要進行注入的控件信息,爲每一個注入編寫代碼
// 添加View控件的綁定代碼
for (ViewBinding binding : viewBindings) {
addViewBinding(constructor, binding, debuggable);
}
進入addViewBinding方法,這邊分了兩種情況
- 只存在控件的綁定
- 存在控件綁定和方法的綁定
對控件的綁定主要是通過編寫findViewById的代碼
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
if (binding.isSingleFieldBinding()) {
//若只有屬性的綁定,沒有方法的綁定
FieldViewBinding fieldBinding = requireNonNull(binding.getFieldBinding());
// 對target中被註解修飾的屬性進行賦值
CodeBlock.Builder builder = CodeBlock.builder()
.add("target.$L = ", fieldBinding.getName());
boolean requiresCast = requiresCast(fieldBinding.getType());
//debug && (requecast || isRequired)
if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
//如果非debug狀態或者字段不需要進行強轉和字段並且不是必需賦值
if (requiresCast) {
//需要向下造型
builder.add("($T) ", fieldBinding.getType());
}
builder.add("source.findViewById($L)", binding.getId().code);
} else {
//debug模式並且需要強轉或者是參數是必需賦值的
// Utils.findRequiredViewAsType(source,[id], "描述", T)
builder.add("$T.find", UTILS);
builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
if (requiresCast) {
builder.add("AsType");
}
builder.add("(source, $L", binding.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;
}
// View屬性有值綁定並且有事件綁定
List<MemberViewBinding> requiredBindings = binding.getRequiredBindings();
// 給局部view變量賦值
if (!debuggable || requiredBindings.isEmpty()) {
result.addStatement("view = source.findViewById($L)", binding.getId().code);
} else if (!binding.isBoundToRoot()) {
result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,
binding.getId().code, asHumanDescription(requiredBindings));
}
// 添加屬性綁定
addFieldBinding(result, binding, debuggable);
//添加事件綁定
addMethodBindings(result, binding, debuggable);
}
接下來看看事件方法的綁定,這邊代碼比較多,我們挑主要的講
- 一個View可能會設置多個事件監聽,所以第一個for循環是遍歷當前View需要註冊的所有事件,爲每一個事件創建一個對應的事件匿名類,然後將事件監聽器綁定到控件。
- 每一個事件監聽器可能會有多個回調方法,因此第第二層for循環就是給匿名事件類添加所有的回調方法
- 在target類中,同一個控件的id可以多次綁定在不同的方法上,因此第三層for循環就是在事件監聽器回調方法中去回調每一個target中當前viewid綁定的方法
- target中事件綁定的方法可能會有多個形參,所以第四層循環是爲了給調用target的事件綁定方法傳入的所有參數
private void addMethodBindings(MethodSpec.Builder result, ViewBinding binding,
boolean debuggable) {
Map<ListenerClass, Map<ListenerMethod, Set<MethodViewBinding>>> classMethodBindings =
binding.getMethodBindings();
if (classMethodBindings.isEmpty()) {
return;
}
// 控件是否可以爲空,爲空添加判斷代碼
boolean needsNullChecked = binding.getRequiredBindings().isEmpty();
if (needsNullChecked) {
result.beginControlFlow("if (view != null)");
}
// 對存在事件監聽的控件需要將控件設置爲成員變量,以便在unbind及時移除控件的監聽器
String fieldName = "viewSource";
String bindName = "source";
if (!binding.isBoundToRoot()) {
fieldName = "view" + Integer.toHexString(binding.getId().value);
bindName = "view";
}
// 給成員變量賦值
result.addStatement("$L = $N", fieldName, bindName);
//1. 爲當前view設置事件監聽,遍歷所有設置的監聽方法
for (Map.Entry<ListenerClass, Map<ListenerMethod, Set<MethodViewBinding>>> e
: classMethodBindings.entrySet()) {
ListenerClass listener = e.getKey();
Map<ListenerMethod, Set<MethodViewBinding>> methodBindings = e.getValue();
//創建一個事件匿名類
TypeSpec.Builder callback = TypeSpec.anonymousClassBuilder("")
.superclass(ClassName.bestGuess(listener.type()));
//2. 給事件匿名類的所有回調方法綁定方法
for (ListenerMethod method : getListenerMethods(listener)) {
// 事件監聽器接口需要實現的方法
MethodSpec.Builder callbackMethod = MethodSpec.methodBuilder(method.name())
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(bestGuess(method.returnType()));
// 添加方法形參
String[] parameterTypes = method.parameters();
for (int i = 0, count = parameterTypes.length; i < count; i++) {
callbackMethod.addParameter(bestGuess(parameterTypes[i]), "p" + i);
}
boolean hasReturnValue = false;
CodeBlock.Builder builder = CodeBlock.builder();
//得到需要綁定的方法
Set<MethodViewBinding> methodViewBindings = methodBindings.get(method);
if (methodViewBindings != null) {
//3. 在接口實現方法中調用需要target中對用的回調方法
for (MethodViewBinding methodBinding : methodViewBindings) {
if (methodBinding.hasReturnValue()) {
hasReturnValue = true;
builder.add("return "); // TODO what about multiple methods?
}
builder.add("target.$L(", methodBinding.getName());
List<Parameter> parameters = methodBinding.getParameters();
String[] listenerParameters = method.parameters();
//4. 添加方法參數
for (int i = 0, count = parameters.size(); i < count; i++) {
if (i > 0) {
builder.add(", ");
}
Parameter parameter = parameters.get(i);
int listenerPosition = parameter.getListenerPosition();
if (parameter.requiresCast(listenerParameters[listenerPosition])) {
if (debuggable) {
builder.add("$T.castParam(p$L, $S, $L, $S, $L, $T.class)", UTILS,
listenerPosition, method.name(), listenerPosition, methodBinding.getName(), i,
parameter.getType());
} else {
builder.add("($T) p$L", parameter.getType(), listenerPosition);
}
} else {
builder.add("p$L", listenerPosition);
}
}
builder.add(");\n");
}
}
if (!"void".equals(method.returnType()) && !hasReturnValue) {
builder.add("return $L;\n", method.defaultReturn());
}
callbackMethod.addCode(builder.build());
callback.addMethod(callbackMethod.build());
}
boolean requiresRemoval = listener.remover().length() != 0;
String listenerField = null;
if (requiresRemoval) {
TypeName listenerClassName = bestGuess(listener.type());
listenerField = fieldName + ((ClassName) listenerClassName).simpleName();
result.addStatement("$L = $L", listenerField, callback.build());
}
//給控件設置事件監聽器
String targetType = listener.targetType();
if (!VIEW_TYPE.equals(targetType)) {
//不是view,則需要強轉爲指定類型
result.addStatement("(($T) $N).$L($L)", bestGuess(targetType), bindName,
listener.setter(), requiresRemoval ? listenerField : callback.build());
} else {
result.addStatement("$N.$L($L)", bindName, listener.setter(),
requiresRemoval ? listenerField : callback.build());
}
}
if (needsNullChecked) {
result.endControlFlow();
}
}
至此BindView註解解析以及對應ViewBinding類代碼生成整個流程就這樣完成了。