溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Android怎么編寫(xiě)基于編譯時(shí)注解的項(xiàng)目

發(fā)布時(shí)間:2021-09-10 14:22:39 來(lái)源:億速云 閱讀:115 作者:小新 欄目:移動(dòng)開(kāi)發(fā)

這篇文章主要為大家展示了“Android怎么編寫(xiě)基于編譯時(shí)注解的項(xiàng)目”,內(nèi)容簡(jiǎn)而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“Android怎么編寫(xiě)基于編譯時(shí)注解的項(xiàng)目”這篇文章吧。

一、概述

在Android應(yīng)用開(kāi)發(fā)中,我們常常為了提升開(kāi)發(fā)效率會(huì)選擇使用一些基于注解的框架,但是由于反射造成一定運(yùn)行效率的損耗,所以我們會(huì)更青睞于編譯時(shí)注解的框架,例如:

  • butterknife免去我們編寫(xiě)View的初始化以及事件的注入的代碼。

  • EventBus3方便我們實(shí)現(xiàn)組建間通訊。

  • fragmentargs輕松的為fragment添加參數(shù)信息,并提供創(chuàng)建方法。

  • ParcelableGenerator可實(shí)現(xiàn)自動(dòng)將任意對(duì)象轉(zhuǎn)換為Parcelable類(lèi)型,方便對(duì)象傳輸。

類(lèi)似的庫(kù)還有非常多,大多這些的庫(kù)都是為了自動(dòng)幫我們完成日常編碼中需要重復(fù)編寫(xiě)的部分(例如:每個(gè)Activity中的View都需要初始化,每個(gè)實(shí)現(xiàn)Parcelable接口的對(duì)象都需要編寫(xiě)很多固定寫(xiě)法的代碼)。

Android怎么編寫(xiě)基于編譯時(shí)注解的項(xiàng)目

這里并不是說(shuō)上述框架就一定沒(méi)有使用反射了,其實(shí)上述其中部分框架內(nèi)部還是有部分實(shí)現(xiàn)是依賴(lài)于反射的,但是很少而且一般都做了緩存的處理,所以相對(duì)來(lái)說(shuō),效率影響很小。

但是在使用這類(lèi)項(xiàng)目的時(shí)候,有時(shí)候出現(xiàn)錯(cuò)誤會(huì)難以調(diào)試,主要原因還是很多用戶(hù)并不了解這類(lèi)框架其內(nèi)部的原理,所以遇到問(wèn)題時(shí)會(huì)消耗大量的時(shí)間去排查。

那么,于情于理,在編譯時(shí)注解框架這么火的時(shí)刻,我們有理由去學(xué)習(xí):如何編寫(xiě)一個(gè)機(jī)遇編譯時(shí)注解的項(xiàng)目

首先,是為了了解其原理,這樣在我們使用類(lèi)似框架遇到問(wèn)題的時(shí)候,能夠找到正確的途徑去排查問(wèn)題;其次,我們?nèi)绻泻玫南敕?,發(fā)現(xiàn)某些代碼需要重復(fù)創(chuàng)建,我們也可以自己來(lái)寫(xiě)個(gè)框架方便自己日常的編碼,提升編碼效率;***也算是自身技術(shù)的提升。

注:以下使用IDE為Android Studio.

本文將以編寫(xiě)一個(gè)View注入的框架為線(xiàn)索,詳細(xì)介紹編寫(xiě)此類(lèi)框架的步驟。

二、編寫(xiě)前的準(zhǔn)備

在編寫(xiě)此類(lèi)框架的時(shí)候,一般需要建立多個(gè)module,例如本文即將實(shí)現(xiàn)的例子:

  • ioc-annotation 用于存放注解等,Java模塊

  • ioc-compiler 用于編寫(xiě)注解處理器,Java模塊

  • ioc-api 用于給用戶(hù)提供使用的API,本例為Andriod模塊

  • ioc-sample 示例,本例為Andriod模塊

那么除了示例以為,一般要建立3個(gè)module,module的名字你可以自己考慮,上述給出了一個(gè)簡(jiǎn)單的參考。當(dāng)然如果條件允許的話(huà),有的開(kāi)發(fā)者喜歡將存放注解和API這兩個(gè)module合并為一個(gè)module。

對(duì)于module間的依賴(lài),因?yàn)榫帉?xiě)注解處理器需要依賴(lài)相關(guān)注解,所以:

  • ioc-compiler依賴(lài)ioc-annotation

我們?cè)谑褂玫倪^(guò)程中,會(huì)用到注解以及相關(guān)API

  • 所以ioc-sample依賴(lài)ioc-api;ioc-api依賴(lài)ioc-annotation

三、注解模塊的實(shí)現(xiàn)

注解模塊,主要用于存放一些注解類(lèi),本例是模板butterknife實(shí)現(xiàn)View注入,所以本例只需要一個(gè)注解類(lèi):

@Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface BindView {     int value(); }

我們?cè)O(shè)置的保留策略為Class,注解用于Field上。這里我們需要在使用時(shí)傳入一個(gè)id,直接以value的形式進(jìn)行設(shè)置即可。

你在編寫(xiě)的時(shí)候,分析自己需要幾個(gè)注解類(lèi),并且正確的設(shè)置@Target以及@Retention即可。

四、注解處理器的實(shí)現(xiàn)

定義完成注解后,就可以去編寫(xiě)注解處理器了,這塊有點(diǎn)復(fù)雜,但是也算是有章可循的。

該模塊,我們一般會(huì)依賴(lài)注解模塊,以及可以使用一個(gè)auto-service庫(kù)

build.gradle的依賴(lài)情況如下:

dependencies {     compile 'com.google.auto.service:auto-service:1.0-rc2'     compile project (':ioc-annotation') }

auto-service庫(kù)可以幫我們?nèi)ド蒑ETA-INF等信息。

(1)基本代碼

注解處理器一般繼承于AbstractProcessor,剛才我們說(shuō)有章可循,是因?yàn)椴糠执a的寫(xiě)法基本是固定的,如下:

@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){     }

在實(shí)現(xiàn)AbstractProcessor后,process()方法是必須實(shí)現(xiàn)的,也是我們編寫(xiě)代碼的核心部分,后面會(huì)介紹。

我們一般會(huì)實(shí)現(xiàn)getSupportedAnnotationTypes()和getSupportedSourceVersion()兩個(gè)方法,這兩個(gè)方法一個(gè)返回支持的注解類(lèi)型,一個(gè)返回支持的源碼版本,參考上面的代碼,寫(xiě)法基本是固定的。

除此以外,我們還會(huì)選擇復(fù)寫(xiě)init()方法,該方法傳入一個(gè)參數(shù)processingEnv,可以幫助我們?nèi)コ跏蓟恍└割?lèi)類(lèi):

  • Filer mFileUtils; 跟文件相關(guān)的輔助類(lèi),生成JavaSourceCode.

  • Elements mElementUtils;跟元素相關(guān)的輔助類(lèi),幫助我們?nèi)カ@取一些元素相關(guān)的信息。

  • Messager mMessager;跟日志相關(guān)的輔助類(lèi)。

這里簡(jiǎn)單提一下Elemnet,我們簡(jiǎn)單認(rèn)識(shí)下它的幾個(gè)子類(lèi),根據(jù)下面的注釋?zhuān)瑧?yīng)該已經(jīng)有了一個(gè)簡(jiǎn)單認(rèn)知。

Element    - VariableElement //一般代表成員變量   - ExecutableElement //一般代表類(lèi)中的方法   - TypeElement //一般代表代表類(lèi)   - PackageElement //一般代表Package

(2)process的實(shí)現(xiàn)

process中的實(shí)現(xiàn),相比較會(huì)比較復(fù)雜一點(diǎn),一般你可以認(rèn)為兩個(gè)大步驟:

  • 收集信息

  • 生成代理類(lèi)(本文把編譯時(shí)生成的類(lèi)叫代理類(lèi))

什么叫收集信息呢?就是根據(jù)你的注解聲明,拿到對(duì)應(yīng)的Element,然后獲取到我們所需要的信息,這個(gè)信息肯定是為了后面生成JavaFileObject所準(zhǔn)備的。

例如本例,我們會(huì)針對(duì)每一個(gè)類(lèi)生成一個(gè)代理類(lèi),例如MainActivity我們會(huì)生成一個(gè)MainActivity$$ViewInjector。那么如果多個(gè)類(lèi)中聲明了注解,就對(duì)應(yīng)了多個(gè)類(lèi),這里就需要:

  • 一個(gè)類(lèi)對(duì)象,代表具體某個(gè)類(lèi)的代理類(lèi)生成的全部信息,本例中為ProxyInfo

  • 一個(gè)集合,存放上述類(lèi)對(duì)象(到時(shí)候遍歷生成代理類(lèi)),本例中為Map,key為類(lèi)的全路徑。

這里的描述有點(diǎn)模糊沒(méi)關(guān)系,一會(huì)結(jié)合代碼就好理解了。

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類(lèi)型         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; }

首先我們調(diào)用一下mProxyMap.clear();,因?yàn)閜rocess可能會(huì)多次調(diào)用,避免生成重復(fù)的代理類(lèi),避免生成類(lèi)的類(lèi)名已存在異常。

然后,通過(guò)roundEnv.getElementsAnnotatedWith拿到我們通過(guò)@BindView注解的元素,這里返回值,按照我們的預(yù)期應(yīng)該是VariableElement集合,因?yàn)槲覀冇糜诔蓡T變量上。

接下來(lái)for循環(huán)我們的元素,首先檢查類(lèi)型是否是VariableElement.

然后拿到對(duì)應(yīng)的類(lèi)信息TypeElement,繼而生成ProxyInfo對(duì)象,這里通過(guò)一個(gè)mProxyMap進(jìn)行檢查,key為qualifiedName即類(lèi)的全路徑,如果沒(méi)有生成才會(huì)去生成一個(gè)新的,ProxyInfo與類(lèi)是一一對(duì)應(yīng)的。

接下來(lái),會(huì)將與該類(lèi)對(duì)應(yīng)的且被@BindView聲明的VariableElement加入到ProxyInfo中去,key為我們聲明時(shí)填寫(xiě)的id,即View的id。

這樣就完成了信息的收集,收集完成信息后,應(yīng)該就可以去生成代理類(lèi)了。

b.生成代理類(lèi)

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

可以看到生成代理類(lèi)的代碼非常的簡(jiǎn)短,主要就是遍歷我們的mProxyMap,然后取得每一個(gè)ProxyInfo,***通過(guò)mFileUtils.createSourceFile來(lái)創(chuàng)建文件對(duì)象,類(lèi)名為proxyInfo.getProxyClassFullName(),寫(xiě)入的內(nèi)容為proxyInfo.generateJavaCode().

看來(lái)生成Java代碼的方法都在ProxyInfo里面。

c.生成Java代碼

這里我們主要關(guān)注其生成Java代碼的方式。

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

#ProxyInfo //key為id,value為對(duì)應(yīng)的成員變量 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"); }

這里主要就是靠收集到的信息,拼接完成的代理類(lèi)對(duì)象了,看起來(lái)會(huì)比較頭疼,不過(guò)我給出一個(gè)生成后的代碼,對(duì)比著看會(huì)很多。

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));         }     } }

這樣對(duì)著上面代碼看會(huì)好很多,其實(shí)就死根據(jù)收集到的成員變量(通過(guò)@BindView聲明的),然后根據(jù)我們具體要實(shí)現(xiàn)的需求去生成java代碼。

這里注意下,生成的代碼實(shí)現(xiàn)了一個(gè)接口ViewInjector,該接口是為了統(tǒng)一所有的代理類(lèi)對(duì)象的類(lèi)型,到時(shí)候我們需要強(qiáng)轉(zhuǎn)代理類(lèi)對(duì)象為該接口類(lèi)型,調(diào)用其方法;接口是泛型,主要就是傳入實(shí)際類(lèi)對(duì)象,例如MainActivity,因?yàn)槲覀冊(cè)谏纱眍?lèi)中的代碼,實(shí)際上就是實(shí)際類(lèi).成員變量的方式進(jìn)行訪(fǎng)問(wèn),所以,使用編譯時(shí)注解的成員變量一般都不允許private修飾符修飾(有的允許,但是需要提供getter,setter訪(fǎng)問(wèn)方法)。

這里采用了完全拼接的方式編寫(xiě)Java代碼,你也可以使用一些開(kāi)源庫(kù),來(lái)通過(guò)Java api的方式來(lái)生成代碼,例如:javapoet.

A Java API for generating .java source files.

到這里我們就完成了代理類(lèi)的生成,這里任何的注解處理器的編寫(xiě)方式基本都遵循著收集信息、生成代理類(lèi)的步驟。

五、API模塊的實(shí)現(xiàn)

有了代理類(lèi)之后,我們一般還會(huì)提供API供用戶(hù)去訪(fǎng)問(wèn),例如本例的訪(fǎng)問(wèn)入口是

//Activity中  Ioc.inject(Activity);  //Fragment中,獲取ViewHolder中  Ioc.inject(this, view);

模仿了butterknife,***個(gè)參數(shù)為宿主對(duì)象,第二個(gè)參數(shù)為實(shí)際調(diào)用findViewById的對(duì)象;當(dāng)然在Actiivty中,兩個(gè)參數(shù)就一樣了。

API一般如何編寫(xiě)呢?

其實(shí)很簡(jiǎn)單,只要你了解了其原理,這個(gè)API就干兩件事:

  • 根據(jù)傳入的host尋找我們生成的代理類(lèi):例如MainActivity->MainActity$$ViewInjector。

  • 強(qiáng)轉(zhuǎn)為統(tǒng)一的接口,調(diào)用接口提供的方法。

這兩件事應(yīng)該不復(fù)雜,***件事是拼接代理類(lèi)名,然后反射生成對(duì)象,第二件事強(qiáng)轉(zhuǎn)調(diào)用。

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相關(guān)代碼          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); }

代碼很簡(jiǎn)單,拼接代理類(lèi)的全路徑,然后通過(guò)newInstance生成實(shí)例,然后強(qiáng)轉(zhuǎn),調(diào)用代理類(lèi)的inject方法。

這里一般情況會(huì)對(duì)生成的代理類(lèi)做一下緩存處理,比如使用Map存儲(chǔ)下,沒(méi)有再生成,這里我們就不去做了。

這樣我們就完成了一個(gè)編譯時(shí)注解框架的編寫(xiě)。

以上是“Android怎么編寫(xiě)基于編譯時(shí)注解的項(xiàng)目”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI