天天看點

Android如何編寫基于編譯時注解的項目

一、概述

在android應用開發中,我們常常為了提升開發效率會選擇使用一些基于注解的架構,但是由于反射造成一定運作效率的損耗,是以我們會更青睐于編譯時注解的架構,例如:

butterknife免去我們編寫view的初始化以及事件的注入的代碼。

eventbus3友善我們實作組建間通訊。

fragmentargs輕松的為fragment添加參數資訊,并提供建立方法。

parcelablegenerator可實作自動将任意對象轉換為parcelable類型,友善對象傳輸。

類似的庫還有非常多,大多這些的庫都是為了自動幫我們完成日常編碼中需要重複編寫的部分(例如:每個activity中的view都需要初始化,每個實作parcelable接口的對象都需要編寫很多固定寫法的代碼)。

Android如何編寫基于編譯時注解的項目

這裡并不是說上述架構就一定沒有使用反射了,其實上述其中部分架構内部還是有部分實作是依賴于反射的,但是很少而且一般都做了緩存的處理,是以相對來說,效率影響很小。

但是在使用這類項目的時候,有時候出現錯誤會難以調試,主要原因還是很多使用者并不了解這類架構其内部的原理,是以遇到問題時會消耗大量的時間去排查。

那麼,于情于理,在編譯時注解架構這麼火的時刻,我們有理由去學習:如何編寫一個機遇編譯時注解的項目

首先,是為了了解其原理,這樣在我們使用類似架構遇到問題的時候,能夠找到正确的途徑去排查問題;其次,我們如果有好的想法,發現某些代碼需要重複建立,我們也可以自己來寫個架構友善自己日常的編碼,提升編碼效率;最後也算是自身技術的提升。

注:以下使用ide為android studio.

本文将以編寫一個view注入的架構為線索,詳細介紹編寫此類架構的步驟。

二、編寫前的準備

在編寫此類架構的時候,一般需要建立多個module,例如本文即将實作的例子:

ioc-annotation 用于存放注解等,java子產品

ioc-compiler 用于編寫注解處理器,java子產品

ioc-api 用于給使用者提供使用的api,本例為andriod子產品

ioc-sample 示例,本例為andriod子產品

那麼除了示例以為,一般要建立3個module,module的名字你可以自己考慮,上述給出了一個簡單的參考。當然如果條件允許的話,有的開發者喜歡将存放注解和api這兩個module合并為一個module。

對于module間的依賴,因為編寫注解處理器需要依賴相關注解,是以:

ioc-compiler依賴ioc-annotation

我們在使用的過程中,會用到注解以及相關api

是以ioc-sample依賴ioc-api;ioc-api依賴ioc-annotation

三、注解子產品的實作

注解子產品,主要用于存放一些注解類,本例是模闆butterknife實作view注入,是以本例隻需要一個注解類:

@retention(retentionpolicy.class) 

@target(elementtype.field) 

public @interface bindview 

    int value(); 

我們設定的保留政策為class,注解用于field上。這裡我們需要在使用時傳入一個id,直接以value的形式進行設定即可。

你在編寫的時候,分析自己需要幾個注解類,并且正确的設定@target以及@retention即可。

四、注解處理器的實作

定義完成注解後,就可以去編寫注解處理器了,這塊有點複雜,但是也算是有章可循的。

該子產品,我們一般會依賴注解子產品,以及可以使用一個auto-service庫

build.gradle的依賴情況如下:

dependencies { 

    compile 'com.google.auto.service:auto-service:1.0-rc2' 

    compile project (':ioc-annotation') 

auto-service庫可以幫我們去生成meta-inf等資訊。

(1)基本代碼

注解處理器一般繼承于abstractprocessor,剛才我們說有章可循,是因為部分代碼的寫法基本是固定的,如下:

@autoservice(processor.class) 

public class iocprocessor extends abstractprocessor{ 

    private filer mfileutils; 

    private elements melementutils; 

    private messager mmessager; 

    @override 

    public synchronized void init(processingenvironment processingenv){ 

        super.init(processingenv); 

        mfileutils = processingenv.getfiler(); 

        melementutils = processingenv.getelementutils(); 

        mmessager = processingenv.getmessager(); 

    } 

    public set<string> getsupportedannotationtypes(){ 

        set<string> annotationtypes = new linkedhashset<string>(); 

        annotationtypes.add(bindview.class.getcanonicalname()); 

        return annotationtypes; 

    public sourceversion getsupportedsourceversion(){ 

        return sourceversion.latestsupported(); 

    public boolean process(set<? extends typeelement> annotations, roundenvironment roundenv){ 

在實作abstractprocessor後,process()方法是必須實作的,也是我們編寫代碼的核心部分,後面會介紹。

我們一般會實作getsupportedannotationtypes()和getsupportedsourceversion()兩個方法,這兩個方法一個傳回支援的注解類型,一個傳回支援的源碼版本,參考上面的代碼,寫法基本是固定的。

除此以外,我們還會選擇複寫init()方法,該方法傳入一個參數processingenv,可以幫助我們去初始化一些父類類:

filer mfileutils; 跟檔案相關的輔助類,生成javasourcecode.

elements melementutils;跟元素相關的輔助類,幫助我們去擷取一些元素相關的資訊。

messager mmessager;跟日志相關的輔助類。

這裡簡單提一下elemnet,我們簡單認識下它的幾個子類,根據下面的注釋,應該已經有了一個簡單認知。

element  

  - variableelement //一般代表成員變量 

  - executableelement //一般代表類中的方法 

  - typeelement //一般代表代表類 

  - packageelement //一般代表package 

(2)process的實作

process中的實作,相比較會比較複雜一點,一般你可以認為兩個大步驟:

收集資訊

生成代理類(本文把編譯時生成的類叫代理類)

什麼叫收集資訊呢?就是根據你的注解聲明,拿到對應的element,然後擷取到我們所需要的資訊,這個資訊肯定是為了後面生成javafileobject所準備的。

例如本例,我們會針對每一個類生成一個代理類,例如mainactivity我們會生成一個mainactivity$$viewinjector。那麼如果多個類中聲明了注解,就對應了多個類,這裡就需要:

一個類對象,代表具體某個類的代理類生成的全部資訊,本例中為proxyinfo

一個集合,存放上述類對象(到時候周遊生成代理類),本例中為map,key為類的全路徑。

這裡的描述有點模糊沒關系,一會結合代碼就好了解了。

a.收集資訊

private map<string, proxyinfo> mproxymap = new hashmap<string, proxyinfo>(); 

@override 

public boolean process(set<? extends typeelement> annotations, roundenvironment roundenv){ 

    mproxymap.clear(); 

    set<? extends element> elements = roundenv.getelementsannotatedwith(bindview.class); 

    //一、收集資訊 

    for (element element : elements){ 

        //檢查element類型 

        if (!checkannotationusevalid(element)){ 

            return false; 

        } 

        //field type 

        variableelement variableelement = (variableelement) element; 

        //class type 

        typeelement typeelement = (typeelement) variableelement.getenclosingelement();//typeelement 

        string qualifiedname = typeelement.getqualifiedname().tostring(); 

        proxyinfo proxyinfo = mproxymap.get(qualifiedname); 

        if (proxyinfo == null){ 

            proxyinfo = new proxyinfo(melementutils, typeelement); 

            mproxymap.put(qualifiedname, proxyinfo); 

        bindview annotation = variableelement.getannotation(bindview.class); 

        int id = annotation.value(); 

        proxyinfo.minjectelements.put(id, variableelement); 

    return true; 

首先我們調用一下mproxymap.clear();,因為process可能會多次調用,避免生成重複的代理類,避免生成類的類名已存在異常。

然後,通過roundenv.getelementsannotatedwith拿到我們通過@bindview注解的元素,這裡傳回值,按照我們的預期應該是variableelement集合,因為我們用于成員變量上。

接下來for循環我們的元素,首先檢查類型是否是variableelement.

然後拿到對應的類資訊typeelement,繼而生成proxyinfo對象,這裡通過一個mproxymap進行檢查,key為qualifiedname即類的全路徑,如果沒有生成才會去生成一個新的,proxyinfo與類是一一對應的。

接下來,會将與該類對應的且被@bindview聲明的variableelement加入到proxyinfo中去,key為我們聲明時填寫的id,即view的id。

這樣就完成了資訊的收集,收集完成資訊後,應該就可以去生成代理類了。

b.生成代理類

    //...省略收集資訊的代碼,以及try,catch相關 

    for(string key : mproxymap.keyset()){ 

        proxyinfo proxyinfo = mproxymap.get(key); 

        javafileobject sourcefile = mfileutils.createsourcefile( 

                proxyinfo.getproxyclassfullname(), proxyinfo.gettypeelement()); 

            writer writer = sourcefile.openwriter(); 

            writer.write(proxyinfo.generatejavacode()); 

            writer.flush(); 

            writer.close(); 

可以看到生成代理類的代碼非常的簡短,主要就是周遊我們的mproxymap,然後取得每一個proxyinfo,最後通過mfileutils.createsourcefile來建立檔案對象,類名為proxyinfo.getproxyclassfullname(),寫入的内容為proxyinfo.generatejavacode().

看來生成java代碼的方法都在proxyinfo裡面。

c.生成java代碼

這裡我們主要關注其生成java代碼的方式。

下面主要看生成java代碼的方法:

#proxyinfo 

//key為id,value為對應的成員變量 

public map<integer, variableelement> minjectelements = new hashmap<integer, variableelement>(); 

public string generatejavacode(){ 

    stringbuilder builder = new stringbuilder(); 

    builder.append("package " + mpackagename).append(";\n\n"); 

    builder.append("import com.zhy.ioc.*;\n"); 

    builder.append("public class ").append(mproxyclassname).append(" implements " + suffix + "<" + mtypeelement.getqualifiedname() + ">"); 

    builder.append("\n{\n"); 

    generatemethod(builder); 

    builder.append("\n}\n"); 

    return builder.tostring(); 

private void generatemethod(stringbuilder builder){ 

     builder.append("public void inject("+mtypeelement.getqualifiedname()+" host , object object )"); 

    for(int id : minjectelements.keyset()){ 

        variableelement variableelement = minjectelements.get(id); 

        string name = variableelement.getsimplename().tostring(); 

        string type = variableelement.astype().tostring() ; 

        builder.append(" if(object instanceof android.app.activity)"); 

        builder.append("\n{\n"); 

        builder.append("host."+name).append(" = "); 

        builder.append("("+type+")(((android.app.activity)object).findviewbyid("+id+"));"); 

        builder.append("\n}\n").append("else").append("\n{\n"); 

        builder.append("("+type+")(((android.view.view)object).findviewbyid("+id+"));"); 

        builder.append("\n}\n"); 

這裡主要就是靠收集到的資訊,拼接完成的代理類對象了,看起來會比較頭疼,不過我給出一個生成後的代碼,對比着看會很多。

package com.zhy.ioc_sample; 

import com.zhy.ioc.*; 

public class mainactivity$$viewinjector implements viewinjector<com.zhy.ioc_sample.mainactivity>{ 

    public void inject(com.zhy.sample.mainactivity host , object object ){ 

        if(object instanceof android.app.activity){ 

            host.mtv = (android.widget.textview)(((android.app.activity)object).findviewbyid(2131492945)); 

        else{ 

            host.mtv = (android.widget.textview)(((android.view.view)object).findviewbyid(2131492945)); 

這樣對着上面代碼看會好很多,其實就死根據收集到的成員變量(通過@bindview聲明的),然後根據我們具體要實作的需求去生成java代碼。

這裡注意下,生成的代碼實作了一個接口viewinjector,該接口是為了統一所有的代理類對象的類型,到時候我們需要強轉代理類對象為該接口類型,調用其方法;接口是泛型,主要就是傳入實際類對象,例如mainactivity,因為我們在生成代理類中的代碼,實際上就是實際類.成員變量的方式進行通路,是以,使用編譯時注解的成員變量一般都不允許private修飾符修飾(有的允許,但是需要提供getter,setter通路方法)。

這裡采用了完全拼接的方式編寫java代碼,你也可以使用一些開源庫,來通過java api的方式來生成代碼,例如:javapoet.

a java api for generating .java source files. 

到這裡我們就完成了代理類的生成,這裡任何的注解處理器的編寫方式基本都遵循着收集資訊、生成代理類的步驟。

五、api子產品的實作

有了代理類之後,我們一般還會提供api供使用者去通路,例如本例的通路入口是

//activity中 

 ioc.inject(activity); 

 //fragment中,擷取viewholder中 

 ioc.inject(this, view); 

模仿了butterknife,第一個參數為宿主對象,第二個參數為實際調用findviewbyid的對象;當然在actiivty中,兩個參數就一樣了。

api一般如何編寫呢?

其實很簡單,隻要你了解了其原理,這個api就幹兩件事:

根據傳入的host尋找我們生成的代理類:例如mainactivity->mainactity$$viewinjector。

強轉為統一的接口,調用接口提供的方法。

這兩件事應該不複雜,第一件事是拼接代理類名,然後反射生成對象,第二件事強轉調用。

public class ioc{ 

    public static void inject(activity activity){ 

        inject(activity , activity); 

    public static void inject(object host , object root){ 

        class<?> clazz = host.getclass(); 

        string proxyclassfullname = clazz.getname()+"$$viewinjector"; 

       //省略try,catch相關代碼  

        class<?> proxyclazz = class.forname(proxyclassfullname); 

        viewinjector viewinjector = (com.zhy.ioc.viewinjector) proxyclazz.newinstance(); 

        viewinjector.inject(host,root); 

public interface viewinjector<t>{ 

    void inject(t t , object object); 

代碼很簡單,拼接代理類的全路徑,然後通過newinstance生成執行個體,然後強轉,調用代理類的inject方法。

這裡一般情況會對生成的代理類做一下緩存處理,比如使用map存儲下,沒有再生成,這裡我們就不去做了。

這樣我們就完成了一個編譯時注解架構的編寫。

六、總結

本文通過具體的執行個體來描述了如何編寫一個基于編譯時注解的項目,主要步驟為:項目結構的劃分、注解子產品的實作、注解處理器的編寫以及對外公布的api子產品的編寫。通過文本的學習應該能夠了解基于編譯時注解這類架構運作的原理,以及自己如何去編寫這樣一類架構。

本文作者:佚名

來源:51cto

繼續閱讀