一、本文需要解決的問題
我研究Butterknife原始碼的目的是為了解決以下幾個我在使用過程中所思考的問題:
- 在很多文章中都提到Butterknife使用編譯時註解技術,什麼是編譯時註解?
- 是完全不呼叫findViewById()等方法了嗎?
- 為什麼繫結各種view時不能使用private修飾?
- 繫結監聽事件的時候方法命名有限制嗎?
二、初步分析
基於Butterknife 8.8.1版本。 為了更好地分析程式碼,我寫了一個demo: MainActivity.java:
public class MainActivity extends Activity {
@BindView(R.id.text)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@OnClick(R.id.text)
public void textClick() {
Toast.makeText(MainActivity.this, "textview clicked", Toast.LENGTH_LONG);
}
}
複製程式碼
我們從Butterknife.bind()方法,即方法入口開始分析: ButterKnife#bind():
@NonNull @UiThread
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();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
// !!!
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 (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
// !!!
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
複製程式碼
程式碼還是比較清晰的,bind()方法的流程:
- 首先獲取當前activity的sourceView,其實就是獲取Activity的DecorView,DecorView是整個ViewTree的最頂層View,包含標題view和內容view這兩個子元素。我們一直呼叫的setContentView()方法其實就是往內容view中新增view元素。
- 然後呼叫createBinding() --> findBindingConstructorForClass(),重點是
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
BINDINGS.put(cls, bindingCtor);
複製程式碼
按照所寫的程式碼,這裡會載入一個MainActivity_ViewBinding類,然後獲取這個類裡面的雙引數(Activity, View)構造方法,最後放在BINDINGS裡面,它是一個map,主要作用是快取。在下次使用的時候,就可以從快取中獲取到:
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
複製程式碼
三、關於編譯時註解
在上面分析過程中,我們知道最後我們會去載入一個MainActivity_ViewBinding類,而這個類並不是我們自己編寫的,而是通過編譯時註解(APT - Annotation Processing Tool)的技術生成的。 這一節將會介紹一下這個技術。
1、什麼是註解
註解其實很常見,比如說Activity自動生成的onCreate()方法上面就有一個@Override註解
- 註解的概念: 能夠新增到 Java 原始碼的語法後設資料。類、方法、變數、引數、包都可以被註解,可用來將資訊後設資料與程式元素進行關聯。
- 註解的分類:
- 標準註解,如Override, Deprecated,SuppressWarnings等
- 元註解,如@Retention, @Target, @Inherited, @Documented。當我們要自定義註解時,需要使用它們
- 自定義註解,表示自己根據需要定義的 Annotation
- 註解的作用:
- 標記,用於告訴編譯器一些資訊
- 編譯時動態處理,如動態生成java程式碼
- 執行時動態處理,如得到註解資訊
2、執行時註解 vs 編譯時註解
一般有些人提到註解,普遍就會覺得效能低下。但是真正使用註解的開源框架卻很多例如ButterKnife,Retrofit等等。所以註解是好是壞呢? 首先,並不是註解就等於效能差。更確切的說是執行時註解這種方式,由於它的原理是java反射機制,所以的確會造成較為嚴重的效能問題。 但是像Butterknife這個框架,它使用的技術是編譯時註解,它不會影響app實際執行的效能(影響的應該是編譯時的效率)。 一句話總結:
- 執行時註解就是在應用執行的過程中,動態地獲取相關類,方法,引數等資訊,由於使用java反射機制,效能會有問題;
- 編譯時註解由於是在程式碼編譯過程中對註解進行處理,通過註解獲取相關類,方法,引數等資訊,然後在專案中生成程式碼,執行時呼叫,其實和直接執行手寫程式碼沒有任何區別,也就沒有效能問題了。 這樣我們就解決了第一個問題。
3、如何使用編譯時註解技術
這裡要藉助到一個類:AbstractProcessor
public class TestProcessor extends AbstractProcessor
{
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
// TODO Auto-generated method stub
return false;
}
}
複製程式碼
重點是process()方法,它相當於每個處理器的主函式main(),可以在這裡寫相關的掃描和處理註解的程式碼,他會幫助生成相關的Java檔案。後面我們可以具體看一下Butterknife中的使用。
四、進一步分析MainActivity_ViewBinding
我們瞭解了編譯時註解的基本概念之後,我們先看一下MainActivity_ViewBinding類具體實現了什麼。 在編寫完demo之後,需要先build一下專案,之後可以在build/generated/source/apt/debug/包名/下面找到這個類,如圖所示:
接上面的分析,到最後會通過反射的方式去呼叫MainActivity_ViewBinding的構造方法。我們直接看這個類的構造方法:@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
// 1
view = Utils.findRequiredView(source, R.id.text, "field 'textView' and method 'textClick'");
// 2
target.textView = Utils.castView(view, R.id.text, "field 'textView'", TextView.class);
// 3
view2131165290 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.textClick();
}
});
}
複製程式碼
1、findRequiredView()
public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(id);
if (view != null) {
return view;
}
String name = getResourceEntryName(source, id);
throw new IllegalStateException("Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
+ " (methods) annotation.");
}
複製程式碼
看到這裡我們已經解決了第二個問題:到最後還是會呼叫findViewById()方法,並沒有完全捨棄這個方法,這裡的source代表著在上面程式碼中傳入的MainActivity的DecorView。大家可以嘗試一下將Activity轉化為Fragment的情況~
2、Util.castView
在這裡,我們解決了第三個問題,繫結各種view時不能使用private修飾,而是需要用public或default去修飾,因為如果採用private修飾的話,將無法通過物件.成員變數方式獲取到我們需要繫結的View。 Util#castView():
public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
try {
return cls.cast(view);
} catch (ClassCastException e) {
String name = getResourceEntryName(view, id);
throw new IllegalStateException("View '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was of the wrong type. See cause for more info.", e);
}
}
複製程式碼
這裡直接呼叫Class.cast強制轉換型別,將View轉化為我們需要的view(TextView)。
3、
view2131165290 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.textClick();
}
});
複製程式碼
這裡會生成一個成員變數來儲存我們需要繫結的View,重點是下面它會呼叫setOnClickListener()方法,傳入的是DebouncingOnClickListener:
/**
* A {@linkplain View.OnClickListener click listener} that debounces multiple clicks posted in the
* same frame. A click on one button disables all buttons for that frame.
*/
public abstract class DebouncingOnClickListener implements View.OnClickListener {
static boolean enabled = true;
private static final Runnable ENABLE_AGAIN = new Runnable() {
@Override public void run() {
enabled = true;
}
};
@Override
public final void onClick(View v) {
if (enabled) {
enabled = false;
v.post(ENABLE_AGAIN);
doClick(v);
}
}
public abstract void doClick(View v);
}
複製程式碼
這個DebouncingOnClickListener是View.OnClickListener的一個子類,作用是防止一定時間內對view的多次點選,即防止快速點選控制元件所帶來的一些不可預料的錯誤。個人認為這個類寫的非常巧妙,既完美解決了問題,又寫的十分優雅,一點都不臃腫。 這裡抽象了doClick()方法,實現程式碼中是直接呼叫了target.textClick(),這裡解決了第四個問題:繫結監聽事件的時候方法命名是沒有限制的,不一定需要嚴格命名為onClick,也不一定需要傳入View引數。
五、MainActivity_ViewBinding的生成
上文提到,MainActivity_ViewBinding類是通過編譯時註解技術生成的,我們找到Butterknife相關的繼承於AbstractProcessor的類,ButterKnifeProcessor,我們直接看process()方法:
public final class ButterKnifeProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// 1
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
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;
}
}
複製程式碼
1、findAndParseTargets() 這個方法的作用是處理所有的@BindXX註解,我們直接看處理@BindView的部分:
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
// 省略程式碼
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
// we don't SuperficialValidation.validateElement(element)
// so that an unresolved View type can be generated by later processing rounds
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
// 省略程式碼
}
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();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
Name qualifiedName = enclosingElement.getQualifiedName();
Name simpleName = element.getSimpleName();
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
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;
}
// Assemble information on the field.
int id = element.getAnnotation(BindView.class).value();
BindingSet.Builder builder = builderMap.get(enclosingElement);
QualifiedId qualifiedId = elementToQualifiedId(element, id);
if (builder != null) {
String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
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(getId(qualifiedId), new FieldViewBinding(name, type, required));
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement);
}
複製程式碼
程式碼邏輯是處理獲取相關注解的資訊,比如繫結的資源id等等,然後通過獲取BindingSet.Builder類的例項來建立一一對應的關係,這裡有一個判斷,如果builderMap存在相應例項則直接取出builder,否則通過getOrCreateBindingBuilder()方法生成一個新的builder,最後呼叫builder.addField()方法。
後續的話返回到findAndParseTargets()方法的最後一部分:
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
// bindView()
// Associate superclass binders with their subclass binders. This is a queue-based tree walk
// which starts at the roots (superclasses) and walks to the leafs (subclasses).
Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
new ArrayDeque<>(builderMap.entrySet());
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);
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;
}
複製程式碼
這裡會生成一個bindingMap,key為TypeElement,代表註解元素型別,value為BindSet類,通過上述的builder.build()生成,BindingSet類中儲存了很多資訊,例如繫結view的型別,生成類的className等等,方便我們後續生成java檔案。最後回到process方法:
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
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;
}
複製程式碼
最後通過brewJava()方法生成java程式碼。 這裡使用到的是javapoet。javapoet是一個開源庫,通過處理相應註解來生成最後的java檔案,這裡是專案地址傳送門,具體技術不再分析。
這篇文章會同步到我的個人日誌,如有問題,請大家踴躍提出,謝謝大家!