簡介
在之前簡單分析了xUtils的View模塊注入,其通過註解,在程序運行時去獲取註解的成員及方法,再通過反射及動態代理實現View的注入和監聽器的綁定。這些都是在運行過程中進行的,難免會影響程序的性能。
而今天要分析的ButterKnife也是通過註解實現View模塊的注入,但不同的是,它是在編譯期生成View注入的代碼,從而實現注入。也就是通過註解註釋將要注入的View和方法,在編譯期間生成findViewById(…)和setListener(…)的代碼,在編譯期間做註解處理,而程序運行時的性能消耗也就很小。
關於ButterKnife的使用就不多詳細介紹,可以直接看官方文檔
生成的代碼
我項目中使用ButterKnife的Activity:
public class MainActivity extends AppCompatActivity {
@Bind(R.id.tv) TextView tv;
@Bind(R.id.btn)Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
tv.setText(".....");
}
@OnClick(R.id.btn)
public void onBtnClick(View view) {
Toast.makeText(MainActivity.this, "Btn click", Toast.LENGTH_SHORT).show();
}
}
生成的代碼:
位於項目目錄下DemoButterknife\app\build\intermediates\incremental-verifier\debug\com\yzw\demobutterknife
// Generated code from Butter Knife. Do not modify!
package com.yzw.demobutterknife;
import android.view.View;
import butterknife.ButterKnife.Finder;
import butterknife.ButterKnife.ViewBinder;
public class MainActivity$$ViewBinder<T extends com.yzw.demobutterknife.MainActivity> implements ViewBinder<T> {
@Override
public void bind(final Finder finder, final T target, Object source) {
View view;
view = finder.findRequiredView(source, 2131492944, "field 'tv'");
target.tv = finder.castView(view, 2131492944, "field 'tv'");
view = finder.findRequiredView(source, 2131492945, "field 'btn' and method 'onBtnClick'");
target.btn = finder.castView(view, 2131492945, "field 'btn'");
view.setOnClickListener(new butterknife.internal.DebouncingOnClickListener() {
@Override public void doClick(android.view.View p0) {
target.onBtnClick(p0);
}
});
}
@Override
public void unbind(T target) {
target.tv = null;
target.btn = null;
}
}
這裏先進行部分解釋:view = finder.findRequiredView(source, 2131492944, "field 'tv'")
可以看到根據source即對應Activity或View等目標資源和控件id(來源於R文件中8進制轉換而來),找到對應id的控件,再進行強轉,這裏可以看到爲什麼要進行再次強轉,因爲在找到對應View的時候不知道器類型,所以也就需要通過直接賦值實現強轉,但是可以看到target.tv,target就是保存對應該控件的目標,從MainActivity$$ViewBinder<T extends com.yzw.demobutterknife.MainActivity>
可以看出該target爲對應Activity,類似,也可以是對應View和Dialog。直接通過target.tv = ...
來進行強制,這也爲什麼通過註解的成員變量不能被private修飾符修飾的原因,想想這可能是一個缺點,可能不符合平時寫代碼的規範,但是在平時注意下即可。
看下Bind註解,可以知道其生存期在編譯期間:
@Retention(CLASS) @Target(FIELD)
public @interface Bind {
/** View ID to which the field will be bound. */
int[] value();
}
看下ViewBinder<T>
接口
/** DO NOT USE: Exposed for generated code. */
public interface ViewBinder<T> {
void bind(Finder finder, T target, Object source);
void unbind(T target);
}
可以看到其生成MainActivity$$ViewBinder
類來實現該接口,在bind(…)方法中生成findViewById(…)和setListener(…)的代碼,關於生產的代碼中個參數和變量所代表的意義可以見上面代碼。
看下Finder(只看主要代碼),是個枚舉類:
public enum Finder {
VIEW {
@Override protected View findView(Object source, int id) { return ((View) source).findViewById(id);}
@Override public Context getContext(Object source) { return ((View) source).getContext(); } },
ACTIVITY {
@Override protected View findView(Object source, int id) { return ((Activity) source).findViewById(id); }
@Override public Context getContext(Object source) { return (Activity) source;} },
DIALOG {
@Override protected View findView(Object source, int id) { return ((Dialog) source).findViewById(id);}
@Override public Context getContext(Object source) { return ((Dialog) source).getContext(); } };
/**
* 相當於findViewById,其中source相當於Activity或者View或者Dialog
*/
public <T> T findRequiredView(Object source, int id, String who) {
T view = findOptionalView(source, id, who);
if (view == null) {
// id錯誤,拋出參數異常IllegalStateException
// 代碼省略...
}
return view;
}
public <T> T findOptionalView(Object source, int id, String who) {
View view = findView(source, id);
return castView(view, id, who);
}
@SuppressWarnings("unchecked") // That's the point.
public <T> T castView(View view, int id, String who) {
try {
return (T) view;
} catch (ClassCastException e) {
// 類型轉換錯誤,拋出參數異常IllegalStateException
// 代碼省略...
}
}
protected abstract View findView(Object source, int id);
public abstract Context getContext(Object source);
}
從上面可以看出ButterKnife在編譯期間生成MainActivity$$ViewBinder
類,其中包括findViewById和setListenr等代碼,從而做到真正的簡化操作,性能開銷方面也不是很大
編譯期註解處理
看到這裏,你可能有疑問,ButterKnife是怎麼生成上面代碼的?
這裏主要通過ButterKnifeProcessor
來實現,它繼承於AbstractProcessor
,是編譯期間的註解處理器,功能非常強大。
主要看下ButterKnifeProcessor
的process(...)
方法,該方法在編譯期間執行,可以獲取所有相關注解,並做處理,這裏便是獲取相關@Bind
註解並生成代碼
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// K:TypeElement 代表等待注入的類元素
// V:BindingClass 該類包含待注入元素的集合
Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
// 循環
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingClass bindingClass = entry.getValue();
try {
JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
Writer writer = jfo.openWriter();
// 生成相應代碼
writer.write(bindingClass.brewJava());
writer.flush();
writer.close();
} catch (IOException e) {
// 拋出異常...
}
}
看下findAndParseTargets
方法,主要是找到所有註解,並整合成Map<TypeElement, BindingClass>
類型
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
// K:TypeElement 代表等待注入的類元素
// V:BindingClass 該類包含待注入元素的集合
Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<TypeElement, BindingClass>();
// 存儲已經解析的TypeElement(只是標記)
Set<String> erasedTargetNames = new LinkedHashSet<String>();
// Process each @Bind element.
for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
try {
parseBind(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, Bind.class, e);
}
}
// Process each annotation that corresponds to a listener.
for (Class<? extends Annotation> listener : LISTENERS) {
findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
}
// Process each @BindBool element.
for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
try {
parseResourceBool(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindBool.class, e);
}
}
// Process each @BindColor element.
// ...
// Process each @BindDimen element.
// ...
// Process each @BindDrawable element.
// ...
// Process each @BindInt element.
// ...
// Process each @BindString element.
// ...
// Try to find a parent binder for each.
// ...
return targetClassMap;
}
可以看到findAndParseTargets
方法解析各種註解,
先看下parseBind
的主要邏輯
解析@Bind註解
private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<String> erasedTargetNames) {
// 參數判斷...
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.ARRAY) {
parseBindMany(element, targetClassMap, erasedTargetNames);
} else if (LIST_TYPE.equals(doubleErasure(elementType))) {
parseBindMany(element, targetClassMap, erasedTargetNames);
} else if (isSubtypeOfType(elementType, ITERABLE_TYPE)) {
// 打出異常信息...
} else {
parseBindOne(element, targetClassMap, erasedTargetNames);
}
}
可以看到parseBind
解析ARRAY
和List
和單個@Bind
,這裏主要看下parseBindOne
Set<String> erasedTargetNames) {
// 獲取該元素element所在的類enclosingElement
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 參數檢查...
// Assemble information on the field.
int[] ids = element.getAnnotation(Bind.class).value();
// 參數檢查...
int id = ids[0];
// 下面代碼邏輯:
// 根據Map<K,V> targetClassMap來獲取對應的BindingClass
// 再將帶有@Bind註解的元素信息添加進BindingClass
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass != null) {
ViewBindings viewBindings = bindingClass.getViewBinding(id);
if (viewBindings != null) {
Iterator<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator();
if (iterator.hasNext()) {
FieldViewBinding existingBinding = iterator.next();
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
Bind.class.getSimpleName(), id, existingBinding.getName(),
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
}
} else {
bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
}
String name = element.getSimpleName().toString();
String type = elementType.toString();
boolean required = isRequiredBinding(element);
// 創建FieldViewBinding代編一個View的注入,並添加到BindingClass中
FieldViewBinding binding = new FieldViewBinding(name, type, required);
bindingClass.addField(id, binding);
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement.toString());
}
private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,
TypeElement enclosingElement) {
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass == null) {
String targetType = enclosingElement.getQualifiedName().toString();
String classPackage = getPackageName(enclosingElement);
String className = getClassName(enclosingElement, classPackage) + SUFFIX;
bindingClass = new BindingClass(classPackage, className, targetType);
targetClassMap.put(enclosingElement, bindingClass);
}
return bindingClass;
}
在這裏,看下構造BindingClass的方法getOrCreateTargetClass
結合上面分析,可以看到BindingClass
代表的是生成的MainActivity$$ViewBinder
類的信息
private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,
TypeElement enclosingElement) {
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass == null) {
String targetType = enclosingElement.getQualifiedName().toString();
String classPackage = getPackageName(enclosingElement);、
// SUFFIX = "$$ViewBinder"
String className = getClassName(enclosingElement, classPackage) + SUFFIX;
bindingClass = new BindingClass(classPackage, className, targetType);
targetClassMap.put(enclosingElement, bindingClass);
}
return bindingClass;
}
到這裏,先來理解一下其他類
從上面可以看到FieldViewBinding
類代表綁定view的類型和字段名(如上面的tv和btn)
看下上面的BindingClass的addField
void addField(int id, FieldViewBinding binding) {
getOrCreateViewBindings(id).addFieldBinding(binding);
}
BindingClass的getOrCreateViewBindings
生成一個ViewBindings:其根據id代表View所綁定的相關信息,比如有監聽器方法,字段類型FieldViewBinding
等
private ViewBindings getOrCreateViewBindings(int id) {
// 先查找是否會該id的ViewBingdings類,否則則創建
ViewBindings viewId = viewIdMap.get(id);
if (viewId == null) {
viewId = new ViewBindings(id);
viewIdMap.put(id, viewId);
}
return viewId;
}
到這裏,小結一下BindingClass、ViewBinds、FieldViewBinding
- BindingClass:代表的是生成的
MainActivity$$ViewBinder
類的信息 - ViewBinds:代表一個控件(id)的相關信息,有該控件字段信息,監聽器方法
- FieldViewBinding :代表某控件的類型和字段名
到這裏@Bind
註解的解析大致瞭解了一下
解析監聽器註解
現在來看下關於監聽器的解析
private void findAndParseListener(RoundEnvironment env,
Class<? extends Annotation> annotationClass, Map<TypeElement, BindingClass> targetClassMap,
Set<String> erasedTargetNames) {
// 先通過env.getElementsAnnotatedWith(annotationClass)
// 獲取所有該註解的元素(即方法),再調用parseListenerAnnotation解析
for (Element element : env.getElementsAnnotatedWith(annotationClass)) {
try {
parseListenerAnnotation(annotationClass, element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
// 輸出異常信息
}
}
}
parseListenerAnnotation
方法有200多行,所以只看重點:
private void parseListenerAnnotation(Class<? extends Annotation> annotationClass, Element element,
Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames)
throws Exception {
// 根據註解獲取各種相關信息,以及各種類型檢查
MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required);
BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
for (int id : ids) {
if (!bindingClass.addMethod(id, listener, method, binding)) {
//輸出異常信息...
}
}
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement.toString());
}
可以看到其根據註解生成一個MethodViewBinding
,再加之添加到BindingClass中。相信從名字上大家也可以知道MethodViewBinding
代表各空間監聽器的方法。這裏簡單瞭解一下:
new MethodViewBinding(name, Arrays.asList(parameters), required);
第一個參數代表可監聽器相應的方法(爲字符串類型),第二個參數爲該相應方法待傳入的參數,第三個參數爲是否執行(默認爲true)
解析資源註解
根據上面View跟監聽器的綁定,同理,關於資源的綁定的思路也是一樣,這裏只分析布爾資源的獲取:
private void parseResourceBool(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<String> erasedTargetNames) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 信息換取,類型檢查...
BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
FieldResourceBinding binding = new FieldResourceBinding(id, name, "getBoolean");
bindingClass.addResource(binding);
erasedTargetNames.add(enclosingElement.toString());
}
可以看到關於資源信息的類FieldResourceBinding
,其封裝了資源id、字段名和相應獲取資源方法"getBoolean"
代碼的生成
看到應該知道Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
Map集合代表什麼了吧。
- TypeElement:代表使用了ButterKnife註解的元素信息,可以把它理解成一個Activity或者View
- BindingClass:代表該元素(Activity或者View)中,註解的使用信息,如上面分析的View注入信息,監聽器信息,資源信息等。
那麼有了這裏信息,怎麼生成源代碼呢:
JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
Writer writer = jfo.openWriter();
writer.write(bindingClass.brewJava());
可以看到,通過filer.createSourceFile
創建一個文件(即 MainActivity$$ViewBinde
相關文件),再通過bindingClass.brewJava()
生成代碼片段並寫入
看下brewJava()
String brewJava() {
StringBuilder builder = new StringBuilder();
builder.append("// Generated code from Butter Knife. Do not modify!\n");
builder.append("package ").append(classPackage).append(";\n\n");
if (!resourceBindings.isEmpty()) {
builder.append("import android.content.res.Resources;\n");
}
if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
builder.append("import android.view.View;\n");
}
builder.append("import butterknife.ButterKnife.Finder;\n");
if (parentViewBinder == null) {
builder.append("import butterknife.ButterKnife.ViewBinder;\n");
}
builder.append('\n');
builder.append("public class ").append(className);
builder.append("<T extends ").append(targetClass).append(">");
if (parentViewBinder != null) {
builder.append(" extends ").append(parentViewBinder).append("<T>");
} else {
builder.append(" implements ViewBinder<T>");
}
builder.append(" {\n");
emitBindMethod(builder);
builder.append('\n');
emitUnbindMethod(builder);
builder.append("}\n");
return builder.toString();
}
可以看到跟前面的MainActivity$$ViewBinde
格式一模一樣,emitBindMethod(builder)
方法則生成對應的Bind方法
而emitUnbindMethod(builder);
方法則生成對應的unBind
方法
現在各種註解信息都是BindingClass中,就是隻是生成findViewById和setListener方法了
程序運行時的調用
ButterKnife的使用是在Activity的onCreate()方法中調用ButterKnife.bind(this);
其最終會調用下面方法:
static void bind(Object target, Object source, Finder finder) {
Class<?> targetClass = target.getClass();
try {
if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
// 找到對應的ViewBinder,即MainActivity$$ViewBinder,
// 隨後調用其bind方法來執行findViewById和setListerner
ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
if (viewBinder != null) {
viewBinder.bind(finder, target, source);
}
} catch (Exception e) {
throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
}
}
可以看到程序最終在bind(...)
方法調用了findViewById和setListener方法
總結
在理解ButterKnife之前我是抵制使用註解來注入的,但是理解了ButterKnife後,我發現到了註解的魅力所在。
到這裏,ButterKnife源碼分析告一段落,相信你可以看到ButterKnife發揮註解的強大之處,能夠將煩躁的findViewById等代碼在編譯期間生成出來,提高了開發效率,何樂而不爲,但是使用時不能關會用,要知道原理,這也是很重要的。
最後,關於本文,如有不足之處或者錯誤的地方,歡迎指出,謝謝。