概述
Android編譯時註解框架從入門到專案實踐。該系列將通過5篇部落格一步步教你打造一個屬於自己的編譯時註解框架,並在之後開源出基於APT的編譯時註解框架。
提到註解,普遍都會有兩種態度:黑科技、低效能。使用註解往往可以實現用非常少的程式碼作出匪夷所思的事情,比如這些框架:ButterKnife、Retrofit。但一直被人詬病的是,執行時註解會因為java反射而引起較為嚴重的效能問題...
今天我們要講的是,不會對效能有任何影響的黑科技:編譯時註解。也有人叫它程式碼生成,其實他們還是有些區別的,在編譯時對註解做處理,通過註解,獲取必要資訊,在專案中生成程式碼,執行時呼叫,和直接執行手寫程式碼沒有任何區別。而更準確的叫法:APT - Annotation Processing Tool
得當的使用編譯時註解,可以極大的提高開發效率,避免編寫重複、易錯的程式碼。大部分時候編譯時註解都可以代替java反射,利用可以直接呼叫的程式碼代替反射,極大的提升執行效率。
本章作為《Android編譯時註解框架》系列的第一章,將分三個部分讓你簡單認識註解框架。之後我們會一步步的建立屬於自己的編譯時註解框架。
-
什麼是註解
-
執行時註解的簡單使用
-
編譯時註解框架ButterKnife原始碼初探
什麼是註解
註解你一定不會陌生,這就是我們最常見的註解:
首先註解分為三類:
-
標準 Annotation
包括 Override, Deprecated, SuppressWarnings,是java自帶的幾個註解,他們由編譯器來識別,不會進行編譯, 不影響程式碼執行,至於他們的含義不是這篇部落格的重點,這裡不再講述。
-
元 Annotation
@Retention, @Target, @Inherited, @Documented,它們是用來定義 Annotation 的 Annotation。也就是當我們要自定義註解時,需要使用它們。
-
自定義 Annotation
根據需要,自定義的Annotation。而自定義的方式,下面我們會講到。
同樣,自定義的註解也分為三類,通過元Annotation - @Retention 定義:
-
@Retention(RetentionPolicy.SOURCE)
原始碼時註解,一般用來作為編譯器標記。如Override, Deprecated, SuppressWarnings。
-
@Retention(RetentionPolicy.RUNTIME)
執行時註解,在執行時通過反射去識別的註解。
-
@Retention(RetentionPolicy.CLASS)
編譯時註解,在編譯時被識別並處理的註解,這是本章重點。
執行時註解的簡單使用
執行時註解的實質是,在程式碼中通過註解進行標記,執行時通過反射尋找標記進行某種處理。而執行時註解一直以來被嘔病的原因便是反射的低效。
下面展示一個Demo。其功能是通過註解實現佈局檔案的設定。
之前我們是這樣設定佈局檔案的:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
}
複製程式碼
如果使用註解,我們就可以這樣設定佈局了
@ContentView(R.layout.activity_home)
public class HomeActivity extends BaseActivity {
。。。
}
複製程式碼
我們先不講這兩種方式哪個好哪個壞,我們只談技術不談需求。
那麼這樣的註解是怎麼實現的呢?很簡單,往下看。
建立一個註解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ContentView {
int value();
}
複製程式碼
第一行:@Retention(RetentionPolicy.RUNTIME)
@Retention用來修飾這是一個什麼型別的註解。這裡表示該註解是一個執行時註解。這樣APT就知道啥時候處理這個註解了。
第二行:@Target({ElementType.TYPE})
@Target用來表示這個註解可以使用在哪些地方。比如:類、方法、屬性、介面等等。這裡ElementType.TYPE 表示這個註解可以用來修飾:Class, interface or enum declaration。當你用ContentView修飾一個方法時,編譯器會提示錯誤。
第三行:public @interface ContentView
這裡的interface並不是說ContentView是一個介面。就像申明類用關鍵字class。申明列舉用enum。申明註解用的就是@interface。(值得注意的是:在ElementType的分類中,class、interface、Annotation、enum同屬一類為Type,並且從官方註解來看,似乎interface是包含@interface的)
/** Class, interface (including annotation type), or enum declaration */
TYPE,
複製程式碼
第四行:int value();
返回值表示這個註解裡可以存放什麼型別值。比如我們是這樣使用的
@ContentView(R.layout.activity_home)
複製程式碼
R.layout.activity_home實質是一個int型id,如果這樣用就會報錯:
@ContentView(“string”)
複製程式碼
關於註解的具體語法,這篇不在詳述,統一放到《Android編譯時註解框架-語法講解》中
註解解析
註解申明好了,但具體是怎麼識別這個註解並使用的呢?
@ContentView(R.layout.activity_home)
public class HomeActivity extends BaseActivity {
。。。
}
複製程式碼
註解的解析就在BaseActivity中。我們看一下BaseActivity程式碼
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//註解解析
for (Class c = this.getClass(); c != Context.class; c = c.getSuperclass()) {
ContentView annotation = (ContentView) c.getAnnotation(ContentView.class);
if (annotation != null) {
try {
this.setContentView(annotation.value());
} catch (RuntimeException e) {
e.printStackTrace();
}
return;
}
}
}
複製程式碼
第一步:遍歷所有的子類
第二步:找到修飾了註解ContentView的類
第三步:獲取ContentView的屬性值。
第四步:為Activity設定佈局。
總結
相信你現在對執行時註解的使用一定有了一些理解了。也知道了執行時註解被人嘔病的地方在哪了。
你可能會覺得*setContentView(R.layout.activity_home)和@ContentView(R.layout.activity_home)*沒什麼區別,用了註解反而還增加了效能問題。
但你要知道,這只是註解最簡單的應用方式。舉一個例子:AndroidEventBus的註解是執行時註解,雖然會有一點效能問題,但是在開發效率上是有提高的。
因為這篇部落格的重點不是執行時註解,所以我們不對其原始碼進行解析。有興趣的可以去github搜一下看看哦~話說AndroidEventBus還是我一個學長寫得,haha~。
編譯時註解框架ButterKnife原始碼初探
ButterKnife大家應該都很熟悉的吧,9000多顆start,讓我們徹底告別了枯燥的findViewbyId。它的使用方式是這樣的:
你難道就沒有好奇過,它是怎麼實現的嗎?嘿嘿,這就是編譯時註解-程式碼生成的黑科技所在了。
祕密在這裡,編譯工程後,開啟你的專案app目錄下的build目錄:
你可以看到一些帶有*$$ViewBinder*字尾的類檔案。這個就是ButterKnife生成的程式碼我們開啟它:
上面有一條註釋: // Generated code from Butter Knife. Do not modify!
1.ForgetActivity$$ViewBinder 和 我們的 ForgetActivity同在一個包下:
package com.zhaoxuan.wehome.view.activity;
複製程式碼
同在一個包下的意義是什麼呢?ForgetActivity$$ViewBinder 可以直接使用 ForgetActivity protected級別以上的屬性方法。就像這樣:
//accountEdit是ForgetActivity當中定義的控制元件
target.accountEdit = finder.castView(view, 2131558541, "field 'accountEdit'");
複製程式碼
所以你也應該知道了為什麼當使用private時會報錯了吧?
2.我們不去管細節,只是大概看一下這段生成的程式碼是什麼意思。我把解析寫在註釋裡。
@Override
public void bind(final Finder finder, final T target, Object source) {
//定義了一個View物件引用,這個物件引用被重複使用了(這可是一個偷懶的寫法哦~)
View view;
//暫時不管Finder是個什麼東西,反正就是一種類似於findViewById的操作。
view = finder.findRequiredView(source, 2131558541, "field 'accountEdit'");
//target就是我們的ForgetActivity,為ForgetActivity中的accountEdit賦值
target.accountEdit = finder.castView(view, 2131558541, "field 'accountEdit'");
view = finder.findRequiredView(source, 2131558543, "field 'forgetBtn' and method 'forgetOnClick'");
target.forgetBtn = finder.castView(view, 2131558543, "field 'forgetBtn'");
//給view設定一個點選事件
view.setOnClickListener(
new butterknife.internal.DebouncingOnClickListener() {
@Override
public void doClick(android.view.View p0) {
//forgetOnClick()就是我們在ForgetActivity中寫得事件方法。
target.forgetOnClick();
}
});
}
複製程式碼
OK,現在你大致明白了ButterKnife的祕密了吧?通過自動生成程式碼的方式來代替我們去寫findViewById這樣繁瑣的程式碼。現在你一定在疑惑兩個問題:
1.這個bind方法什麼時候被呼叫?我們的程式碼裡並沒有ForgetActivity$$ViewBinder 這種奇怪的類引用呀。
2.Finder到底是個什麼東西?憑什麼它可以找到view。
不著急不著急,慢慢看。
註解: @Bind的定義
我們可以解讀的資訊如下:
-
Bind是編譯時註解
-
只能修飾屬性
-
屬性值是一個int型的陣列。
建立好自定義註解,之後我們就可以通過APT去識別解析到這些註解,並且可以通過這些註解得到註解的值、註解所修飾的類的型別、名稱。註解所在類的名稱等等資訊。
Finder類
通過上面生成的程式碼,你一定奇怪,Finder到底是個什麼東西。Finder實際是一個列舉。
根據不同型別的,提供了不同實現的findView和getContext方法。這裡你終於看到了熟悉的findViewById了吧,哈哈,祕密就在這裡。
另外Finder還有兩個重要的方法,也是剛才沒有介紹清楚的: finder.findRequiredView 和 finder.castView
findRequiredView 方法呼叫了 findOptionalView 方法
findOptionalView呼叫了不同列舉類實現的findView方法(實際上就是findViewById啦~)
findView取得view後,又交給了castView做一些容錯處理。
castView上來啥都不幹直接強轉並return。如果發生異常,就執行catch方法,只是丟擲異常而已,我們就不看了。
ButterKnife.bind(this)方法
*ButterKnife.bind(this)*這個方法我們通常都在BaseActivity的onCreate方法中呼叫,似乎所有的findViewById方法,都被這一個bind方法化解了~
bind有幾個過載方法,但最終調的都是下面這個方法。
引數target一般是我們的Activity,source是用來獲取Context查詢資源的。當target是activity時,Finder是Finder.ACTIVITY。
首先取得target,(也就是Activity)的Class物件,根據Class物件找到生成的類,例如:ForgetActivity$$ViewBinder。
然後呼叫ForgetActivity$$ViewBinder的bind方法。
然後就沒有啦~看到這裡你就大致明白了在程式執行過程中ButterKnife的實現原理了。下面上重頭戲,ButterKnife編譯時所做的工作。
ButterKnifeProcessor
你可能在疑惑,ButterKnife是如何識別註解的,又是如何生成程式碼的。
AbstractProcessor是APT的核心類,所有的黑科技,都產生在這裡。AbstractProcessor只有兩個最重要的方法process 和 getSupportedAnnotationTypes。
重寫getSupportedAnnotationTypes方法,用來表示該AbstractProcessor類處理哪些註解。
第一個最明顯的就是Bind註解啦。
而所有的註解處理,都是在process中執行的:
通過findAndParseTargets方法拿到所有需要被處理的註解集合。然後對其進行遍歷。
JavaFileObject是我們程式碼生成的關鍵物件,它的作用是寫java檔案。ForgetActivity$$ViewBinder這種奇怪的類檔案,就是用JavaFileObject來生成的。
這裡我們只關注最重要的一句話
writer.write(bindingClass.brewJava());
複製程式碼
ForgetActivity$$ViewBinder中所有程式碼,都是通過bindingClass.brewJava方法拼出來的。
bindingClass.brewJava方法
哎,我不知道你看到這個程式碼時候,是什麼感覺。反正我看到這個時候腦袋裡只有一句話:好low啊……
我根本沒想到這麼黑科技高大上的東西居然是這麼寫出來的。一行程式碼一行程式碼往出拼啊……
既然知道是字串拼接起來的,就沒有看下去的心思了,這裡就不放完整程式碼了。
由此,你也知道了之前看生成的程式碼,為什麼是用了偷懶的方法寫了吧~
總結
當你揭開一個不熟悉領域的面紗後,黑科技好像也不過如此,甚至用字串拼接出來的程式碼感覺lowlow的。
但這不正是學習的魅力麼?
好了,總結一下。
-
編譯時註解的魅力在於:編譯時按照一定策略生成程式碼,避免編寫重複程式碼,提高開發效率,且不影響效能。
-
程式碼生成與程式碼插入(Aspectj)是有區別的。程式碼插入面向切面,是在程式碼執行前後插入程式碼,新產生的程式碼是由原有程式碼觸發的。而程式碼生成只是自動產生一套獨立的程式碼,程式碼的執行還是需要主動呼叫才可以。
-
APT是一套非常強大的機制,它唯一的限制在於你天馬行空的設計~
-
ButterKnife的原理其實很簡單,可是為什麼這麼簡單的功能,卻寫了那麼多程式碼呢?因為ButterKnife作為一個外部依賴框架,做了大量的容錯和效驗來保證執行穩定。所以:寫一個框架最難的不是技術實現,而是穩定!
-
ButterKnife有一個非常值得借鑑的地方,就是如何用生成的程式碼對已有的程式碼進行代理執行。這個如果你在研究有代理功能的APT框架的話,應該好好研究一下。
APT就好像一塊蛋糕擺在你面前,就看你如何優雅的吃了。
後續篇章我將會陸續推出幾款以Cake命名的APT框架。