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

2016-07-17 | 暴打小女孩| Android

概述

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源碼初探

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的定義

咱們能夠解讀的信息以下:

  1. Bind是編譯時註解

  2. 只能修飾屬性

  3. 屬性值是一個int型的數組。

建立好自定義註解,以後咱們就能夠經過APT去識別解析到這些註解,而且能夠經過這些註解獲得註解的值、註解所修飾的類的類型、名稱。註解所在類的名稱等等信息。

Finder類

經過上面生成的代碼,你必定奇怪,Finder究竟是個什麼東西。Finder實際是一個枚舉。

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

另外Finder還有兩個重要的方法,也是剛纔沒有介紹清楚的: finder.findRequiredViewfinder.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的。

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

好了,總結一下。

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

  2. 代碼生成與代碼插入(Aspectj)是有區別的。代碼插入面向切面,是在代碼運行先後插入代碼,新產生的代碼是由原有代碼觸發的。而代碼生成只是自動產生一套獨立的代碼,代碼的執行仍是須要主動調用才能夠。

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

  4. ButterKnife的原理其實很簡單,但是爲何這麼簡單的功能,卻寫了那麼多代碼呢?由於ButterKnife做爲一個外部依賴框架,作了大量的容錯和效驗來保證運行穩定。因此:寫一個框架最難的不是技術實現,而是穩定!

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

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

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


原地址

相關文章
相關標籤/搜索