ButterKnife源碼分析
前言
在N久之前,自從實驗室里面的學長推薦我用butterknife后, 從此的項目再也離不開butterknife了,然而自以為對它很熟時,前不久今日頭條實習生招聘二面卻被面試官洗刷了一頓。然后整個二面完全是被虐的感覺,估計最后會掛,哎!
當時被問到butterknife的實現,懵逼的我想都不想就答上了注解加反射。然而面試官卻一臉疑問的問我:你確定?除了反射還有其他方法么????
我了個去,難道butterknife不是用的反射??難道還有其他方法來實現這玩意兒么?不行,面試完了趕快clone 源碼下來看看。不看不知道,一看嚇一跳,原來還真不是用的注解加反射。在此感謝面試官為我開啟了新世界的大門,原來注解還能這么用!
Butterknife用法
我相信學過android開發應該基本上都用過Butterknife吧,就算沒用過也聽說過吧?畢竟是大名鼎鼎的Jake Wharton出品的東西。要是沒用過的話可以看看這里,里面雖然是講的Annotation,但是例子就是用注解加反射實現的低級的Butterknife。哈哈!用法里面大概也說了下。
Butterknife原理
講到butterknife的原理。這里不得不提一下一般這種注入框架都是運行時注解,即聲明注解的生命周期為RUNTIME,然后在運行的時候通過反射完成注入,這種方式雖然簡單,但是這種方式多多少少會有性能的損耗。那么有沒有一種方法能解決這種性能的損耗呢? 沒錯,答案肯定是有的,那就是Butterknife用的APT(Annotation Processing Tool)編譯時解析技術。
APT大概就是你聲明的注解的生命周期為CLASS,然后繼承AbstractProcessor類。繼承這個類后,在編譯的時候,編譯器會掃描所有帶有你要處理的注解的類,然后再調用AbstractProcessor的process方法,對注解進行處理,那么我們就可以在處理的時候,動態生成綁定事件或者控件的java代碼,然后在運行的時候,直接調用bind方法完成綁定。
其實這種方式的好處是我們不用再一遍一遍地寫findViewById和onClick了,這個框架在編譯的時候幫我們自動生成了這些代碼,然后在運行的時候調用就行了。
源碼解析
上面講了那么多,其實都不如直接解析源碼來得直接,下面我們就一步一步來探究大神怎樣實現Butterknife的吧。
拿到源碼的第一步是從我們調用的地方來突破,那我們就來看看程序里面是怎樣調用它的呢?
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.setDebug(true);
ButterKnife.bind(this);
// Contrived code to use the bound fields.
title.setText("Butter Knife");
subtitle.setText("Field and method binding for Android views.");
footer.setText("by Jake Wharton");
hello.setText("Say Hello");
adapter = new SimpleAdapter(this);
listOfThings.setAdapter(adapter);
}</code></pre>
上面是github上給的例子,我們直接就從 ButterKnife.bind(this)
入手吧,點進來看看:
public static Unbinder bind(@NonNull Activity target) {
return bind(target, target, Finder.ACTIVITY);
}
咦?我再點:
static Unbinder bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) {
Class<?> targetClass = target.getClass();
try {
ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
return viewBinder.bind(finder, target, source);
} catch (Exception e) {
throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
}
}
好吧,bind方法主要就是拿到我們綁定的Activity的Class,然后找到這個Class的ViewBinder,最后調用ViewBinder的bind()
方法,那么問題來了,ViewBinder是個什么鬼???我們打開
findViewBinderForClass()
方法。
@NonNull
private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
throws IllegalAccessException, InstantiationException {
ViewBinder<Object> viewBinder = BINDERS.get(cls);
if (viewBinder != null) {
return viewBinder;
}
String clsName = cls.getName();
try {
Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
} catch (ClassNotFoundException e) {
viewBinder = findViewBinderForClass(cls.getSuperclass());
}
BINDERS.put(cls, viewBinder);
return viewBinder;
}
這里我去掉了一些Log信息,保留了關鍵代碼,上面的BINDERS是一個保存了Class為key,Class$$ViewBinder
為Value的一個LinkedHashMap,主要是做一下緩存,提高下次再來bind的性能。
在第10行的時候,clsName 是我們傳入要綁定的Activity類名,這里相當于拿到了Activity$$ViewBinder
這個東西,這個類又是什么玩意兒?其實從類名可以看出來,相當于Activity的一個內部類,這時候我們就要問了,我們在用的時候沒有聲明這個類啊???從哪里來的? 不要方,其實它就是我們在之前講原理的時候說到的AbstractProcessor在編譯的時候生成的一個類,我們后面再來看它,現在我們繼續往下面分析。在第11行就用反射反射了一個viewBinder 實例出來。
剛剛說了,這個方法里面用linkhashMap做了下緩存,所以在15行的時候,就把剛剛反射的viewBinder作為value,Class作為key加入這個LinkedHashMap,下次再bind這個類的時候,就直接在第4行的時候取出來用,提升性能。
現在返回剛剛的bind方法,我們拿到了這個Activity的viewBinder,然后調用它的bind方法。咦?這就完了???我們再點進viewBinder的bind方法看看。
public interface ViewBinder<T> {
Unbinder bind(Finder finder, T target, Object source);
}
什么,接口???什么鬼?剛剛不是new了一個viewBinder出來么?然后這里就調用了這個viewBinder的bind方法, 不行,我要看一下bind到底是什么鬼!上面說了,Butterknife用了APT技術,那么這里的viewBinder應該就是編譯的時候生成的,那么我們就反編譯下apk。看看到底生成了什么代碼:
下面我們就先用一個簡單的綁定TextView的例子,然后反編譯出來看看:
public class MainActivity extends AppCompatActivity {
@Bind(R.id.text_view)
TextView textView;
@OnClick(R.id.text_view)
void onClick(View view) {
textView.setText("我被click了");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
textView.setText("我還沒有被click");
}
}</code></pre>
源代碼就這行幾行,然后反編譯看看:

源代碼就多了一個類,MainActivity$$ViewBinder
,打開看看:
public class MainActivity$$ViewBinder<T extends MainActivity>
implements ButterKnife.ViewBinder<T>
{
public void bind(ButterKnife.Finder paramFinder, final T paramT, Object paramObject)
{
View localView = (View)paramFinder.findRequiredView(paramObject, 2131492944, "field 'textView' and method 'onClick'");
paramT.textView = ((TextView)paramFinder.castView(localView, 2131492944, "field 'textView'"));
localView.setOnClickListener(new DebouncingOnClickListener()
{
public void doClick(View paramAnonymousView)
{
paramT.onClick(paramAnonymousView);
}
});
}
public void unbind(T paramT)
{
paramT.textView = null;
}
}</code></pre>
還記得剛剛說的,反射了一個Class$$ViewBinder
么?看這里的類名。現在應該懂了吧?它剛好也是實現了ButterKnife.ViewBinder<T>
接口,我們說了,在bind方法中,最后調用了ViewBinder的bind方法,先說下幾個參數paramFinder其實就是一個Finder,因為我們可以在Activity中使用butterknife,也可以在Fragment和Adapter等中使用butterknife,那么在不同的地方使用butterknife,這個Finder也就不同。在Activity中,其實源碼 就是這樣子的:
ACTIVITY {
@Override protected View findView(Object source, int id) {
return ((Activity) source).findViewById(id);
}
@Override public Context getContext(Object source) {
return (Activity) source;
}
}</code></pre>
有沒有很熟悉???其實還是用的findViewById
,那么在Dialog和Fragment中,根據不同的地方,實現的方式不同。
這里的paramT和paramObject都是我們要綁定的Activity類,通過代碼可以跟蹤到。
返回上面的ViewBinder代碼,首先調用了Finder的findRequiredView方法,其實這個方法最后經過處理就是調用了findView方法,拿到相應的view,然后再賦值給paramT.textView,剛說了paramT就是那個要綁定的Activity,現在懂了吧?這里通過 paramT.textView 這樣的調用方式,說明了Activity中不能把TextView設置為private,不然會報錯,其實這里可以用反射來拿到textView的,這里大概也是為了性能著想吧。最后setOnClickListener,DebouncingOnClickListener
這個Listener其實也是實現了View.OnClickListener 方法,然后在OnClick里面調用了doClick
方法。流程大概跟蹤了一遍。現在還留下最后一塊了:
Butterknife到底是怎樣在編譯的時候生成代碼的?
我們來看一下它的ButterKnifeProcessor
類:
Init方法:
@Override public synchronized void init(ProcessingEnvironment env) {
super.init(env);
elementUtils = env.getElementUtils();
typeUtils = env.getTypeUtils();
filer = env.getFiler();
}</code></pre>
ProcessingEnviroment參數提供很多有用的工具類Elements, Types和Filer。Types是用來處理TypeMirror的工具類,Filer用來創建生成輔助文件。至于ElementUtils嘛,其實ButterKnifeProcessor在運行的時候,會掃描所有的Java源文件,然后每一個Java源文件的每一個部分都是一個Element,比如一個包、類或者方法。
@Override public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(BindArray.class.getCanonicalName());
types.add(BindBitmap.class.getCanonicalName());
types.add(BindBool.class.getCanonicalName());
types.add(BindColor.class.getCanonicalName());
types.add(BindDimen.class.getCanonicalName());
types.add(BindDrawable.class.getCanonicalName());
types.add(BindInt.class.getCanonicalName());
types.add(BindString.class.getCanonicalName());
types.add(BindView.class.getCanonicalName());
types.add(BindViews.class.getCanonicalName());
for (Class<? extends Annotation> listener : LISTENERS) {
types.add(listener.getCanonicalName());
}
return types;
}</code></pre>
getSupportedAnnotationTypes()方法主要是指定ButterknifeProcessor是注冊給哪些注解的。我們可以看到,在源代碼里面,作者一個一個地把Class文件加到那個LinkedHashSet里面,然后再把LISTENERS也全部加進去。
其實整個類最重要的是process方法:
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingClass bindingClass = entry.getValue();
try {
bindingClass.brewJava().writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
e.getMessage());
}
}
return true;
}</code></pre>
這個方法的作用主要是掃描、評估和處理我們程序中的注解,然后生成Java文件。也就是前面說的ViewBinder。首先一進這個函數就調用了findAndParseTargets
方法,我們就去看看findAndParseTargets
方法到底做了什么:
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseBindView(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
Observable.from(topLevelClasses)
.flatMap(new Func1<BindingClass, Observable<?>>() {
@Override public Observable<?> call(BindingClass topLevelClass) {
if (topLevelClass.hasViewBindings()) {
// It has an unbinder class and it will also be the highest unbinder class for all
// descendants.
topLevelClass.setHighestUnbinderClassName(topLevelClass.getUnbinderClassName());
} else {
// No unbinder class, so null it out so we know we can just return the NOP unbinder.
topLevelClass.setUnbinderClassName(null);
}
// Recursively set up parent unbinding relationships on all its descendants.
return ButterKnifeProcessor.this.setParentUnbindingRelationships(
topLevelClass.getDescendants());
}
})
.toCompletable()
.await();
return targetClassMap;
}</code></pre>
這里代碼炒雞多,我就不全部貼出來了,只貼出來一部分,這個方法最后還用了rxjava的樣子。這個方法的主要的流程如下:
- 掃描所有具有注解的類,然后根據這些類的信息生成BindingClass,最后生成以TypeElement為鍵,BindingClass為值的鍵值對。
- 循環遍歷這個鍵值對,根據TypeElement和BindingClass里面的信息生成對應的java類。例如AnnotationActivity生成的類即為
Cliass$$ViewBinder
類。
因為我們之前用的例子是綁定的一個View,所以我們就只貼了解析View的代碼。好吧,這里遍歷了所有帶有@BindView
的Element,然后對每一個Element進行解析,也就進入了parseBindView
這個方法中:
private void parseBindView(Element element, Map<TypeElement, BindingClass> targetClassMap,
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();
}
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
BindView.class.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
hasError = true;
}
if (hasError) {
return;
}
// Assemble information on the field.
int id = element.getAnnotation(BindView.class).value();
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)",
BindView.class.getSimpleName(), id, existingBinding.getName(),
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
}
} else {
bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
}
String name = element.getSimpleName().toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
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);
}</code></pre>
然后這里從一進入這個方法到
int id = element.getAnnotation(BindView.class).value();
都是在拿到注解信息,然后驗證注解的target的類型是否繼承自view,然后上面這一行代碼獲得我們要綁定的View的id,再從targetClassMap里面取出BindingClass(這個BindingClass是管理了所有關于這個注解的一些信息還有實例本身的信息,其實最后是通過BindingClass來生成java代碼的),如果targetClassMap里面不存在的話,就在
bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
這里生成一個,我們進去看一下getOrCreateTargetClass
:
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);
boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
String className = getClassName(enclosingElement, classPackage) + BINDING_CLASS_SUFFIX;
String classFqcn = getFqcn(enclosingElement) + BINDING_CLASS_SUFFIX;
bindingClass = new BindingClass(classPackage, className, isFinal, targetType, classFqcn);
targetClassMap.put(enclosingElement, bindingClass);
}
return bindingClass;
}</code></pre>
這里面其實很簡單,就是獲取一些這個注解所修飾的變量的一些信息,比如類名呀,包名呀,然后className
這里就賦值成Class$$ViewHolder
了,因為:
private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";
然后把這個解析后的bindingClass加入到targetClassMap里面。
返回剛剛的parseBindView
中,根據view的信息生成一個FieldViewBinding,最后添加到上邊生成的BindingClass實例中。這里基本完成了解析工作。最后回到findAndParseTargets
中:
Observable.from(topLevelClasses)
.flatMap(new Func1<BindingClass, Observable<?>>() {
@Override public Observable<?> call(BindingClass topLevelClass) {
if (topLevelClass.hasViewBindings()) {
topLevelClass.setHighestUnbinderClassName(topLevelClass.getUnbinderClassName());
} else {
topLevelClass.setUnbinderClassName(null);
}
return ButterKnifeProcessor.this.setParentUnbindingRelationships(
topLevelClass.getDescendants());
}
})
.toCompletable()
.await();</code></pre>
這里用到了rxjava,其實這里主要的工作是建立上面的綁定的所有的實例的解綁的關系,因為我們綁定了,最后在代碼中還是會解綁的。這里預先處理好了這些關系。因為這里要遞歸地完成解綁,所以用了flatmap,flatmap把每一個創建出來的 Observable 發送的事件,都集中到同一個 Observable 中,然后這個 Observable 負責將這些事件統一交給 Subscriber 。
然而這部分涉及到很多rxjava的東西,有興趣的童鞋去看看大神的寫給android開發者的RxJava 詳解這篇文章,然后再來看這里就很輕松了。
回到我們的process中, 現在解析完了annotation,該生成java文件了,我再把代碼貼一下:
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingClass bindingClass = entry.getValue();
try {
bindingClass.brewJava().writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
e.getMessage());
}
}
return true;
}
遍歷剛剛得到的targetClassMap ,然后再一個一個地通過
bindingClass.brewJava().writeTo(filer);
來生成java文件。然而生成的java文件也是根據上面的信息來用字符串拼接起來的,然而這個工作在brewJava()中完成了:
JavaFile brewJava() {
TypeSpec.Builder result = TypeSpec.classBuilder(className)
.addModifiers(PUBLIC)
.addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));
if (isFinal) {
result.addModifiers(Modifier.FINAL);
}
if (hasParentBinding()) {
result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentBinding.classFqcn),
TypeVariableName.get("T")));
} else {
result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
}
result.addMethod(createBindMethod());
if (hasUnbinder() && hasViewBindings()) {
// Create unbinding class.
result.addType(createUnbinderClass());
if (!isFinal) {
// Now we need to provide child classes to access and override unbinder implementations.
createUnbinderCreateUnbinderMethod(result);
}
}
return JavaFile.builder(classPackage, result.build())
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
這里用到了java中的javapoet技術,不了解的童鞋可以傳送到github上面,也是square的杰作,這個不在這篇文章的講解范圍內,有興趣的童鞋可以去看看,很不錯的開源項目。
最后通過writeTo(Filer filer)生成java源文件。
寫了好幾個小時終于寫完了,最后希望剛面完HR面的幾個公司不要再掛我了,不然就找不到實習工作了啊啊啊!!!
文/尸情化異(簡書作者)
原文鏈接:http://www.jianshu.com/p/0f3f4f7ca505
著作權歸作者所有,轉載請聯系作者獲得授權,并標注“簡書作者”。