Android編譯時註解框架從入門到項目實踐。該系列將經過5篇博客一步步教你打造一個屬於本身的編譯時註解框架,並在以後開源出基於APT的編譯時註解框架。java
提到註解,廣泛都會有兩種態度:黑科技、低性能。使用註解每每能夠實現用很是少的代碼做出匪夷所思的事情,好比這些框架:ButterKnife、Retrofit。但一直被人詬病的是,運行時註解會由於java反射而引發較爲嚴重的性能問題...android
今天咱們要講的是,不會對性能有任何影響的黑科技:編譯時註解。也有人叫它代碼生成,其實他們仍是有些區別的,在編譯時對註解作處理,經過註解,獲取必要信息,在項目中生成代碼,運行時調用,和直接運行手寫代碼沒有任何區別。而更準確的叫法:APT - Annotation Processing Toolgit
得當的使用編譯時註解,能夠極大的提升開發效率,避免編寫重複、易錯的代碼。大部分時候編譯時註解均可以代替java反射,利用能夠直接調用的代碼代替反射,極大的提高運行效率。github
本章做爲《Android編譯時註解框架》系列的第一章,將分三個部分讓你簡單認識註解框架。以後咱們會一步步的建立屬於本身的編譯時註解框架。數組
什麼是註解app
運行時註解的簡單使用框架
編譯時註解框架ButterKnife源碼初探ide
註解你必定不會陌生,這就是咱們最多見的註解:佈局
首先註解分爲三類:性能
標準 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你們應該都很熟悉的吧,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是編譯時註解
只能修飾屬性
屬性值是一個int型的數組。
建立好自定義註解,以後咱們就能夠經過APT去識別解析到這些註解,而且能夠經過這些註解獲得註解的值、註解所修飾的類的類型、名稱。註解所在類的名稱等等信息。
經過上面生成的代碼,你必定奇怪,Finder究竟是個什麼東西。Finder實際是一個枚舉。
根據不一樣類型的,提供了不一樣實現的findView和getContext方法。這裏你終於看到了熟悉的findViewById了吧,哈哈,祕密就在這裏。
另外Finder還有兩個重要的方法,也是剛纔沒有介紹清楚的: finder.findRequiredView 和 finder.castView
findRequiredView 方法調用了 findOptionalView 方法
findOptionalView調用了不一樣枚舉類實現的findView方法(實際上就是findViewById啦~)
findView取得view後,又交給了castView作一些容錯處理。
castView上來啥都不幹直接強轉並return。若是發生異常,就執行catch方法,只是拋出異常而已,咱們就不看了。
*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編譯時所作的工做。
你可能在疑惑,ButterKnife是如何識別註解的,又是如何生成代碼的。
AbstractProcessor是APT的核心類,全部的黑科技,都產生在這裏。AbstractProcessor只有兩個最重要的方法process 和 getSupportedAnnotationTypes。
重寫getSupportedAnnotationTypes方法,用來表示該AbstractProcessor類處理哪些註解。
第一個最明顯的就是Bind註解啦。
而全部的註解處理,都是在process中執行的:
經過findAndParseTargets方法拿到全部須要被處理的註解集合。而後對其進行遍歷。
JavaFileObject是咱們代碼生成的關鍵對象,它的做用是寫java文件。ForgetActivity$$ViewBinder這種奇怪的類文件,就是用JavaFileObject來生成的。
這裏咱們只關注最重要的一句話
writer.write(bindingClass.brewJava());
複製代碼
ForgetActivity$$ViewBinder中全部代碼,都是經過bindingClass.brewJava方法拼出來的。
哎,我不知道你看到這個代碼時候,是什麼感受。反正我看到這個時候腦殼裏只有一句話:好low啊……
我根本沒想到這麼黑科技高大上的東西竟然是這麼寫出來的。一行代碼一行代碼往出拼啊……
既然知道是字符串拼接起來的,就沒有看下去的心思了,這裏就不放完整代碼了。
由此,你也知道了以前看生成的代碼,爲何是用了偷懶的方法寫了吧~
當你揭開一個不熟悉領域的面紗後,黑科技好像也不過如此,甚至用字符串拼接出來的代碼感受lowlow的。
但這不正是學習的魅力麼?
好了,總結一下。
編譯時註解的魅力在於:編譯時按照必定策略生成代碼,避免編寫重複代碼,提升開發效率,且不影響性能。
代碼生成與代碼插入(Aspectj)是有區別的。代碼插入面向切面,是在代碼運行先後插入代碼,新產生的代碼是由原有代碼觸發的。而代碼生成只是自動產生一套獨立的代碼,代碼的執行仍是須要主動調用才能夠。
APT是一套很是強大的機制,它惟一的限制在於你天馬行空的設計~
ButterKnife的原理其實很簡單,但是爲何這麼簡單的功能,卻寫了那麼多代碼呢?由於ButterKnife做爲一個外部依賴框架,作了大量的容錯和效驗來保證運行穩定。因此:寫一個框架最難的不是技術實現,而是穩定!
ButterKnife有一個很是值得借鑑的地方,就是如何用生成的代碼對已有的代碼進行代理執行。這個若是你在研究有代理功能的APT框架的話,應該好好研究一下。
APT就好像一塊蛋糕擺在你面前,就看你如何優雅的吃了。
後續篇章我將會陸續推出幾款以Cake命名的APT框架。