前言
本篇是系列文章的第四篇呜象,Butterknife源碼全面解析呼寸。上一篇利用反射和注解手?jǐn)]一個(gè)Android依賴注入框架中提到過稳捆,如果我們頻繁使用反射會(huì)導(dǎo)致性能比較差火的,那Butterknife是如何解決這個(gè)問題的呢壶愤?
Butterknife為什么沒有性能問題
源碼開始解析之前,我們先來了解一下Butterknife的核心原理馏鹤。在夯實(shí)基礎(chǔ):Java的注解中我們提到過注解的保留期征椒。
- 源碼時(shí)注解(SOURCE):僅保留在源碼階段,在編譯期就會(huì)被丟棄湃累,一般是編譯器來解析相關(guān)注解
- 編譯器注解(CLASS):保留在編譯階段勃救,在類的加載階段會(huì)被丟棄碍讨,一般使用APT技術(shù)來解析
- 運(yùn)行時(shí)注解(RUNTIME):全程保留,從源碼到App運(yùn)行過程中蒙秒,一般使用反射來解析
了解了前面的知識(shí)勃黍,我們就可以知道Butterknife肯定不是用反射來解析注解的,因?yàn)榉瓷涞男时容^低晕讲,也不是通過編譯器來解析的覆获,如果是這樣的話,壓根也就沒有了Butterknife這個(gè)庫了瓢省。Butterknife是用APT技術(shù)來解析注解的弄息,APT的全稱是Android Annotation Processor Tool,Android注解解析工具勤婚。它是在編譯期階段來解析注解并生成對應(yīng)的代碼摹量,然后在使用的時(shí)候再去調(diào)生成的代碼,這樣就跟我們自己寫好的代碼自己調(diào)用一下馒胆,不會(huì)產(chǎn)生任何性能問題缨称,并且會(huì)減少大量的模板類代碼。(關(guān)注APT技術(shù)的詳解我會(huì)在下一篇手?jǐn)]Butterknife給大家詳細(xì)介紹)
Bnifeknife的源碼解析
注意:本文做的源碼分析的Butterknife版本是最新的10.2.1
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tvTitle)
TextView textView;
@BindView(R.id.button)
Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
textView.setText("文本");
button.setText("按鈕");
}
@OnClick(R.id.button)
public void testClick(){
Toast.makeText(getApplicationContext(),"測試點(diǎn)擊事件",Toast.LENGTH_SHORT).show();
}
}
我們就先以這個(gè)簡答的例子看一下国章,這里我們用了@BindView和@OnClick兩個(gè)注解具钥,單看這兩個(gè)注解我們看不出什么,我們直接跟到ButterKnife.bind(this);里面液兽,最終會(huì)跟到
@NonNull @UiThread
public static Unbinder bind(@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);
}
}
這段代碼核心的就干了三件事
1.Class<?> targetClass = target.getClass(); 拿到傳入的Activity的Class類對象
2.Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);尋找第一步的Class類對象的構(gòu)造方法
3.return constructor.newInstance(target, source);//調(diào)用構(gòu)造方法
這里我們還要從findBindingConstructorForClass(targetClass)跟進(jìn)去看看他是怎么尋找的構(gòu)造方法
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);//重點(diǎn)一
if (bindingCtor != null || BINDINGS.containsKey(cls)) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")
|| clsName.startsWith("androidx.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");//重點(diǎn)二
//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;
}
這里需要看的地方就兩個(gè)骂删,代碼里已經(jīng)加了注釋
重點(diǎn)一:BINDINGS是一個(gè)Map對象,而且是個(gè)靜態(tài)常量(生命周期等同于App)四啰,這個(gè)保證了我們?nèi)绻且呀?jīng)被綁定過的對象宁玫,不需要二次綁定,可以直接從Map里面取出來柑晒,主要是為了性能優(yōu)化考慮欧瘪。
重點(diǎn)二: cls.getClassLoader().loadClass(clsName + "_ViewBinding");這里以我們上面的例子為例,clsName就等于MainActivity匙赞,這里就是用cls的類加載器去加載了一個(gè)名叫MainActivity_ViewBinding的java類佛掖,下面我們就要找一下MainActivity_ViewBinding這個(gè)java類。上面我們提到過涌庭,Butterknife是通過APT技術(shù)來解析注解并生成相關(guān)代碼芥被,我們可以推斷出這個(gè)MainActivity_ViewBinding這個(gè)java類就是Butterknife生成的代碼,通過APT生成的代碼都在如圖所示目錄下
我們點(diǎn)開這個(gè)文件看一下
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view7f070022;
@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;
target.textView = Utils.findRequiredViewAsType(source, R.id.tvTitle, "field 'textView'", TextView.class);
view = Utils.findRequiredView(source, R.id.button, "field 'button' and method 'testClick'");
target.button = Utils.castView(view, R.id.button, "field 'button'", Button.class);
view7f070022 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.testClick();
}
});
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.textView = null;
target.button = null;
view7f070022.setOnClickListener(null);
view7f070022 = null;
}
}
看到這里我們很多事情都會(huì)豁然開朗坐榆,首先我們回憶一下ButterKnife.bind(this);這行代碼干的核心事情是調(diào)用MainActivity_ViewBinding的構(gòu)造方法拴魄,在MainActivity_ViewBinding的構(gòu)造方法里面,我們干了些什么事情呢?
一行一行代碼看
重點(diǎn)一: target.textView = Utils.findRequiredViewAsType(source, R.id.tvTitle, "field 'textView'", TextView.class);直接點(diǎn)到findRequiredViewAsType這個(gè)方法里面看
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
View view = findRequiredView(source, id, who);//findViewById
return castView(view, id, who, cls);//轉(zhuǎn)型
}
重點(diǎn)二:
view = Utils.findRequiredView(source, R.id.button, "field 'button' and method 'testClick'");
target.button = Utils.castView(view, R.id.button, "field 'button'", Button.class);
這里為啥把重點(diǎn)一的一步拆分為了兩步(findViewById和轉(zhuǎn)型)呢匹中,因?yàn)锽utton有個(gè)點(diǎn)擊事件夏漱,且點(diǎn)擊事件和創(chuàng)建View是分開的,也就是說我不創(chuàng)建View也可以聲明點(diǎn)擊事件顶捷,二者沒有影響
重點(diǎn)三:
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.testClick();
}
});
給button設(shè)置點(diǎn)擊事件挂绰,最終調(diào)的是我們寫的testClick()
其實(shí)這段源碼沒有什么可以特別解讀的地方,但是我真的建議大家認(rèn)真的研習(xí)一下服赎,去感覺一下JakeWharton大神寫的代碼是多么的簡潔且優(yōu)雅扮授。
注意:這里也解決了一個(gè)我們在使用Butterknife的疑問,就是我們使用注解的變量或者方法不能被private和protected修飾专肪,因?yàn)槲覀兊膖arget的對象要直接調(diào)用對應(yīng)的變量和方法刹勃,比如 target.textView ; target.testClick();
APT技術(shù)
前面提到了Butterknife使用了APT技術(shù)生成了MainActivity_Binding這個(gè)類嚎尤,下面我簡單介紹一下APT技術(shù)荔仁。
Android在編譯期階段會(huì)掃描所有繼承自AbstractProcess的子類,并調(diào)用其中的process()方法芽死,我們只要在process()方法里拿到我們所有的注解并生成對應(yīng)的代碼即可乏梁。下面我來介紹一下AbstractProcess以及它相關(guān)的幾個(gè)方法
AbstractProcessor是一個(gè)抽象類,常用的方法有四個(gè)
- init(ProcessingEnvironment processingEnv):注解處理器的初始化关贵,一般在這里獲取我們需要的工具類
- process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv):必須要重寫的方法遇骑,也是實(shí)際入口有點(diǎn)類似與main()方法,我們要在這寫我們實(shí)際的業(yè)務(wù)代碼
- getSupportedAnnotationTypes():指定注解處理器是注冊給哪個(gè)注解的揖曾,返回指定支持的注解類集合
- getSupportedSourceVersion():返回支持的jdk版本落萎,一般都直接使用SourceVersion.latestSupported();
Butterknife中APT的應(yīng)用
關(guān)于APT這里只是簡單介紹一下,因?yàn)橄乱黄恼挛視?huì)帶大家手?jǐn)]一個(gè)Butterknife炭剪,這里我們還是回到Butterknife的源碼中來练链。下面的源碼分析需要大家把Butterknife的代碼clone下來,找到ButterKnifeProcessor這個(gè)類奴拦。
ButterKnifeProcessor中的源碼很多媒鼓,這里我們只看重點(diǎn),直接到process方法中
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);//解析所有的Butterknife注解并生成一個(gè)Map對象
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);//這里用到了JavaPoet庫中的類错妖,生成對應(yīng)的java類
try {
javaFile.writeTo(filer);//把java類寫入到文件里面
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
關(guān)鍵的代碼注釋已經(jīng)寫好了绿鸣,這里要特別提一下Butterknife內(nèi)部還引入了一個(gè)Java庫叫JavaPoet,這個(gè)是專門用來編寫java文件的暂氯,對JavaPoet感興趣的小伙伴可以自行Google一下潮模。BindingSet這個(gè)類是對JavaPoet里面的一些屬性的封裝,它包含了一個(gè)Java文件里面包含的各種注解信息株旷,由它生成java文件再寫入即可再登。為了方便理解,我這里再配一張流程圖
總結(jié)
Butterknife不存在性能問題是利用了APT技術(shù)晾剖,在代碼編譯階段就對注解做了解析并生成了相關(guān)代碼锉矢,Butterknife.bind(this);實(shí)際上就是對生成代碼的調(diào)用齿尽。關(guān)于APT技術(shù)的詳細(xì)介紹和如何自定義一個(gè)注解解析器沽损,我將會(huì)在下一篇文章,也是系列文章的最后一篇中給大家?guī)硌罚绻X得還不錯(cuò)绵估,請點(diǎn)個(gè)贊,謝謝~