Android編譯時註解框架系列1-什麼是編譯時註解

餓了麼物流技術團隊發表於2018-03-15
2016-07-17 | 暴打小女孩| Android

概述

Android編譯時註解框架從入門到專案實踐。該系列將通過5篇部落格一步步教你打造一個屬於自己的編譯時註解框架,並在之後開源出基於APT的編譯時註解框架。

提到註解,普遍都會有兩種態度:黑科技、低效能。使用註解往往可以實現用非常少的程式碼作出匪夷所思的事情,比如這些框架:ButterKnife、Retrofit。但一直被人詬病的是,執行時註解會因為java反射而引起較為嚴重的效能問題...

今天我們要講的是,不會對效能有任何影響的黑科技:編譯時註解。也有人叫它程式碼生成,其實他們還是有些區別的,在編譯時對註解做處理,通過註解,獲取必要資訊,在專案中生成程式碼,執行時呼叫,和直接執行手寫程式碼沒有任何區別。而更準確的叫法:APT - Annotation Processing Tool

得當的使用編譯時註解,可以極大的提高開發效率,避免編寫重複、易錯的程式碼。大部分時候編譯時註解都可以代替java反射,利用可以直接呼叫的程式碼代替反射,極大的提升執行效率。

本章作為《Android編譯時註解框架》系列的第一章,將分三個部分讓你簡單認識註解框架。之後我們會一步步的建立屬於自己的編譯時註解框架。

  • 什麼是註解

  • 執行時註解的簡單使用

  • 編譯時註解框架ButterKnife原始碼初探

什麼是註解

註解你一定不會陌生,這就是我們最常見的註解:

Android編譯時註解框架系列1-什麼是編譯時註解

首先註解分為三類:

  • 標準 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。它的使用方式是這樣的:

Android編譯時註解框架系列1-什麼是編譯時註解

你難道就沒有好奇過,它是怎麼實現的嗎?嘿嘿,這就是編譯時註解-程式碼生成的黑科技所在了。

祕密在這裡,編譯工程後,開啟你的專案app目錄下的build目錄:

Android編譯時註解框架系列1-什麼是編譯時註解

你可以看到一些帶有*$$ViewBinder*字尾的類檔案。這個就是ButterKnife生成的程式碼我們開啟它:

Android編譯時註解框架系列1-什麼是編譯時註解

上面有一條註釋: // 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的定義

Android編譯時註解框架系列1-什麼是編譯時註解

我們可以解讀的資訊如下:

  1. Bind是編譯時註解

  2. 只能修飾屬性

  3. 屬性值是一個int型的陣列。

建立好自定義註解,之後我們就可以通過APT去識別解析到這些註解,並且可以通過這些註解得到註解的值、註解所修飾的類的型別、名稱。註解所在類的名稱等等資訊。

Finder類

通過上面生成的程式碼,你一定奇怪,Finder到底是個什麼東西。Finder實際是一個列舉。

Android編譯時註解框架系列1-什麼是編譯時註解

根據不同型別的,提供了不同實現的findView和getContext方法。這裡你終於看到了熟悉的findViewById了吧,哈哈,祕密就在這裡。

另外Finder還有兩個重要的方法,也是剛才沒有介紹清楚的: finder.findRequiredViewfinder.castView

Android編譯時註解框架系列1-什麼是編譯時註解

findRequiredView 方法呼叫了 findOptionalView 方法

findOptionalView呼叫了不同列舉類實現的findView方法(實際上就是findViewById啦~)

findView取得view後,又交給了castView做一些容錯處理。

castView上來啥都不幹直接強轉並return。如果發生異常,就執行catch方法,只是丟擲異常而已,我們就不看了。

ButterKnife.bind(this)方法

*ButterKnife.bind(this)*這個方法我們通常都在BaseActivity的onCreate方法中呼叫,似乎所有的findViewById方法,都被這一個bind方法化解了~

bind有幾個過載方法,但最終調的都是下面這個方法。

Android編譯時註解框架系列1-什麼是編譯時註解

引數target一般是我們的Activity,source是用來獲取Context查詢資源的。當target是activity時,Finder是Finder.ACTIVITY。

首先取得target,(也就是Activity)的Class物件,根據Class物件找到生成的類,例如:ForgetActivity$$ViewBinder

然後呼叫ForgetActivity$$ViewBinder的bind方法。

然後就沒有啦~看到這裡你就大致明白了在程式執行過程中ButterKnife的實現原理了。下面上重頭戲,ButterKnife編譯時所做的工作。

ButterKnifeProcessor

你可能在疑惑,ButterKnife是如何識別註解的,又是如何生成程式碼的。

Android編譯時註解框架系列1-什麼是編譯時註解

AbstractProcessor是APT的核心類,所有的黑科技,都產生在這裡。AbstractProcessor只有兩個最重要的方法process 和 getSupportedAnnotationTypes。

Android編譯時註解框架系列1-什麼是編譯時註解

重寫getSupportedAnnotationTypes方法,用來表示該AbstractProcessor類處理哪些註解。

第一個最明顯的就是Bind註解啦。

而所有的註解處理,都是在process中執行的:

Android編譯時註解框架系列1-什麼是編譯時註解

通過findAndParseTargets方法拿到所有需要被處理的註解集合。然後對其進行遍歷。

JavaFileObject是我們程式碼生成的關鍵物件,它的作用是寫java檔案。ForgetActivity$$ViewBinder這種奇怪的類檔案,就是用JavaFileObject來生成的。

這裡我們只關注最重要的一句話

writer.write(bindingClass.brewJava());
複製程式碼

ForgetActivity$$ViewBinder中所有程式碼,都是通過bindingClass.brewJava方法拼出來的。

bindingClass.brewJava方法

哎,我不知道你看到這個程式碼時候,是什麼感覺。反正我看到這個時候腦袋裡只有一句話:好low啊……

我根本沒想到這麼黑科技高大上的東西居然是這麼寫出來的。一行程式碼一行程式碼往出拼啊……

既然知道是字串拼接起來的,就沒有看下去的心思了,這裡就不放完整程式碼了。

由此,你也知道了之前看生成的程式碼,為什麼是用了偷懶的方法寫了吧~

Android編譯時註解框架系列1-什麼是編譯時註解

總結

當你揭開一個不熟悉領域的面紗後,黑科技好像也不過如此,甚至用字串拼接出來的程式碼感覺lowlow的。

但這不正是學習的魅力麼?

好了,總結一下。

  1. 編譯時註解的魅力在於:編譯時按照一定策略生成程式碼,避免編寫重複程式碼,提高開發效率,且不影響效能。

  2. 程式碼生成與程式碼插入(Aspectj)是有區別的。程式碼插入面向切面,是在程式碼執行前後插入程式碼,新產生的程式碼是由原有程式碼觸發的。而程式碼生成只是自動產生一套獨立的程式碼,程式碼的執行還是需要主動呼叫才可以。

  3. APT是一套非常強大的機制,它唯一的限制在於你天馬行空的設計~

  4. ButterKnife的原理其實很簡單,可是為什麼這麼簡單的功能,卻寫了那麼多程式碼呢?因為ButterKnife作為一個外部依賴框架,做了大量的容錯和效驗來保證執行穩定。所以:寫一個框架最難的不是技術實現,而是穩定!

  5. ButterKnife有一個非常值得借鑑的地方,就是如何用生成的程式碼對已有的程式碼進行代理執行。這個如果你在研究有代理功能的APT框架的話,應該好好研究一下。

APT就好像一塊蛋糕擺在你面前,就看你如何優雅的吃了。

後續篇章我將會陸續推出幾款以Cake命名的APT框架。


原地址

相關文章