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

一、概述

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

  • butterknife 免去我們編寫View的初始化以及事件的注入的代碼。
  • EventBus3 方便我們實現組建間通訊。
  • fragmentargs 輕松的為fragment添加參數信息,并提供創建方法。
  • ParcelableGenerator 可實現自動將任意對象轉換為Parcelable類型,方便對象傳輸。

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

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

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

那么,于情于理,在編譯時注解框架這么火的時刻,我們有理由去學習:

  • 如何編寫一個機遇編譯時注解的項目

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

注:以下使用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();
    }
    @Override
    public Set<String> getSupportedAnnotationTypes(){
        Set<String> annotationTypes = new LinkedHashSet<String>();
        annotationTypes.add(BindView.class.getCanonicalName());
        return annotationTypes;
    }
    @Override
    public SourceVersion getSupportedSourceVersion(){
        return SourceVersion.latestSupported();
    }
    @Override
    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</code></pre>

    (2)process的實現

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

    • 收集信息
    • 生成代理類(本文把編譯時生成的類叫代理類)

    什么叫收集信息呢?就是根據你的注解聲明,拿到對應的Element,然后獲取到我們所需要的信息,這個信息肯定是為了后面生成 JavaFileObject 所準備的。

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

    • 一個類對象,代表具體某個類的代理類生成的全部信息,本例中為 ProxyInfo
    • 一個集合,存放上述類對象(到時候遍歷生成代理類),本例中為 Map<String, ProxyInfo> ,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; }</code></pre>

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

    然后,通過 roundEnv.getElementsAnnotatedWith 拿到我們通過 @BindView 注解的元素,這里返回值,按照我們的預期應該是 VariableElement 集合,因為我們用于成員變量上。

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

    然后拿到對應的類信息 TypeElement ,繼而生成 ProxyInfo 對象,這里通過一個 mProxyMap 進行檢查,key為 qualifiedName 即類的全路徑,如果沒有生成才會去生成一個新的, ProxyInfo 與類是一一對應的。

    接下來,會將與該類對應的且被 @BindView 聲明的 VariableElement 加入到 ProxyInfo 中去,key為我們聲明時填寫的id,即View的id。

    這樣就完成了信息的收集,收集完成信息后,應該就可以去生成代理類了。

    b.生成代理類

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
    //...省略收集信息的代碼,以及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();
    

    } return true; }</code></pre>

    可以看到生成代理類的代碼非常的簡短,主要就是遍歷我們的 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 )"); builder.append("\n{\n"); 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("host."+name).append(" = ");
    builder.append("("+type+")(((android.view.View)object).findViewById("+id+"));");
    builder.append("\n}\n");
}
builder.append("\n}\n");

}</code></pre>

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

package com.zhy.ioc_sample;
import com.zhy.ioc.*;
public class MainActivity$$ViewInjector implements ViewInjector<com.zhy.ioc_sample.MainActivity>{
    @Override
    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<T> ,該接口是為了統一所有的代理類對象的類型,到時候我們需要強轉代理類對象為該接口類型,調用其方法;接口是泛型,主要就是傳入實際類對象,例如 MainActivity ,因為我們在生成代理類中的代碼,實際上就是 實際類.成員變量 的方式進行訪問,所以,使用編譯時注解的成員變量一般都不允許 private 修飾符修飾(有的允許,但是需要提供getter,setter訪問方法)。

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

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模塊的編寫。通過文本的學習應該能夠了解基于編譯時注解這類框架運行的原理,以及自己如何去編寫這樣一類框架。

 

閱讀原文

 

 本文由用戶 duye0629 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!