這篇文章主要為大家展示了“Android怎么編寫基于編譯時(shí)注解的項(xiàng)目”,內(nèi)容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“Android怎么編寫基于編譯時(shí)注解的項(xiàng)目”這篇文章吧。
專注于為中小企業(yè)提供網(wǎng)站建設(shè)、網(wǎng)站制作服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)南宮免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動了成百上千家企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。
一、概述
在Android應(yīng)用開發(fā)中,我們常常為了提升開發(fā)效率會選擇使用一些基于注解的框架,但是由于反射造成一定運(yùn)行效率的損耗,所以我們會更青睞于編譯時(shí)注解的框架,例如:
butterknife免去我們編寫View的初始化以及事件的注入的代碼。
EventBus3方便我們實(shí)現(xiàn)組建間通訊。
fragmentargs輕松的為fragment添加參數(shù)信息,并提供創(chuàng)建方法。
ParcelableGenerator可實(shí)現(xiàn)自動將任意對象轉(zhuǎn)換為Parcelable類型,方便對象傳輸。
類似的庫還有非常多,大多這些的庫都是為了自動幫我們完成日常編碼中需要重復(fù)編寫的部分(例如:每個Activity中的View都需要初始化,每個實(shí)現(xiàn)Parcelable接口的對象都需要編寫很多固定寫法的代碼)。
這里并不是說上述框架就一定沒有使用反射了,其實(shí)上述其中部分框架內(nèi)部還是有部分實(shí)現(xiàn)是依賴于反射的,但是很少而且一般都做了緩存的處理,所以相對來說,效率影響很小。
但是在使用這類項(xiàng)目的時(shí)候,有時(shí)候出現(xiàn)錯誤會難以調(diào)試,主要原因還是很多用戶并不了解這類框架其內(nèi)部的原理,所以遇到問題時(shí)會消耗大量的時(shí)間去排查。
那么,于情于理,在編譯時(shí)注解框架這么火的時(shí)刻,我們有理由去學(xué)習(xí):如何編寫一個機(jī)遇編譯時(shí)注解的項(xiàng)目
首先,是為了了解其原理,這樣在我們使用類似框架遇到問題的時(shí)候,能夠找到正確的途徑去排查問題;其次,我們?nèi)绻泻玫南敕?,發(fā)現(xiàn)某些代碼需要重復(fù)創(chuàng)建,我們也可以自己來寫個框架方便自己日常的編碼,提升編碼效率;***也算是自身技術(shù)的提升。
注:以下使用IDE為Android Studio.
本文將以編寫一個View注入的框架為線索,詳細(xì)介紹編寫此類框架的步驟。
二、編寫前的準(zhǔn)備
在編寫此類框架的時(shí)候,一般需要建立多個module,例如本文即將實(shí)現(xiàn)的例子:
ioc-annotation 用于存放注解等,Java模塊
ioc-compiler 用于編寫注解處理器,Java模塊
ioc-api 用于給用戶提供使用的API,本例為Andriod模塊
ioc-sample 示例,本例為Andriod模塊
那么除了示例以為,一般要建立3個module,module的名字你可以自己考慮,上述給出了一個簡單的參考。當(dāng)然如果條件允許的話,有的開發(fā)者喜歡將存放注解和API這兩個module合并為一個module。
對于module間的依賴,因?yàn)榫帉懽⒔馓幚砥餍枰蕾囅嚓P(guān)注解,所以:
ioc-compiler依賴ioc-annotation
我們在使用的過程中,會用到注解以及相關(guān)API
所以ioc-sample依賴ioc-api;ioc-api依賴ioc-annotation
三、注解模塊的實(shí)現(xiàn)
注解模塊,主要用于存放一些注解類,本例是模板butterknife實(shí)現(xiàn)View注入,所以本例只需要一個注解類:
@Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface BindView { int value(); }
我們設(shè)置的保留策略為Class,注解用于Field上。這里我們需要在使用時(shí)傳入一個id,直接以value的形式進(jìn)行設(shè)置即可。
你在編寫的時(shí)候,分析自己需要幾個注解類,并且正確的設(shè)置@Target以及@Retention即可。
四、注解處理器的實(shí)現(xiàn)
定義完成注解后,就可以去編寫注解處理器了,這塊有點(diǎn)復(fù)雜,但是也算是有章可循的。
該模塊,我們一般會依賴注解模塊,以及可以使用一個auto-service庫
build.gradle的依賴情況如下:
dependencies { compile 'com.google.auto.service:auto-service:1.0-rc2' compile project (':ioc-annotation') }
auto-service庫可以幫我們?nèi)ド蒑ETA-INF等信息。
(1)基本代碼
注解處理器一般繼承于AbstractProcessor,剛才我們說有章可循,是因?yàn)椴糠执a的寫法基本是固定的,如下:
@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 SetgetSupportedAnnotationTypes(){ Set annotationTypes = new LinkedHashSet (); 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)的,也是我們編寫代碼的核心部分,后面會介紹。
我們一般會實(shí)現(xiàn)getSupportedAnnotationTypes()和getSupportedSourceVersion()兩個方法,這兩個方法一個返回支持的注解類型,一個返回支持的源碼版本,參考上面的代碼,寫法基本是固定的。
除此以外,我們還會選擇復(fù)寫init()方法,該方法傳入一個參數(shù)processingEnv,可以幫助我們?nèi)コ跏蓟恍└割愵悾?/p>
Filer mFileUtils; 跟文件相關(guān)的輔助類,生成JavaSourceCode.
Elements mElementUtils;跟元素相關(guān)的輔助類,幫助我們?nèi)カ@取一些元素相關(guān)的信息。
Messager mMessager;跟日志相關(guān)的輔助類。
這里簡單提一下Elemnet,我們簡單認(rèn)識下它的幾個子類,根據(jù)下面的注釋,應(yīng)該已經(jīng)有了一個簡單認(rèn)知。
Element - VariableElement //一般代表成員變量 - ExecutableElement //一般代表類中的方法 - TypeElement //一般代表代表類 - PackageElement //一般代表Package
(2)process的實(shí)現(xiàn)
process中的實(shí)現(xiàn),相比較會比較復(fù)雜一點(diǎn),一般你可以認(rèn)為兩個大步驟:
收集信息
生成代理類(本文把編譯時(shí)生成的類叫代理類)
什么叫收集信息呢?就是根據(jù)你的注解聲明,拿到對應(yīng)的Element,然后獲取到我們所需要的信息,這個信息肯定是為了后面生成JavaFileObject所準(zhǔn)備的。
例如本例,我們會針對每一個類生成一個代理類,例如MainActivity我們會生成一個MainActivity$$ViewInjector。那么如果多個類中聲明了注解,就對應(yīng)了多個類,這里就需要:
一個類對象,代表具體某個類的代理類生成的全部信息,本例中為ProxyInfo
一個集合,存放上述類對象(到時(shí)候遍歷生成代理類),本例中為Map,key為類的全路徑。
這里的描述有點(diǎn)模糊沒關(guān)系,一會結(jié)合代碼就好理解了。
a.收集信息
private MapmProxyMap = new HashMap (); @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; }
首先我們調(diào)用一下mProxyMap.clear();,因?yàn)閜rocess可能會多次調(diào)用,避免生成重復(fù)的代理類,避免生成類的類名已存在異常。
然后,通過roundEnv.getElementsAnnotatedWith拿到我們通過@BindView注解的元素,這里返回值,按照我們的預(yù)期應(yīng)該是VariableElement集合,因?yàn)槲覀冇糜诔蓡T變量上。
接下來for循環(huán)我們的元素,首先檢查類型是否是VariableElement.
然后拿到對應(yīng)的類信息TypeElement,繼而生成ProxyInfo對象,這里通過一個mProxyMap進(jìn)行檢查,key為qualifiedName即類的全路徑,如果沒有生成才會去生成一個新的,ProxyInfo與類是一一對應(yīng)的。
接下來,會將與該類對應(yīng)的且被@BindView聲明的VariableElement加入到ProxyInfo中去,key為我們聲明時(shí)填寫的id,即View的id。
這樣就完成了信息的收集,收集完成信息后,應(yīng)該就可以去生成代理類了。
b.生成代理類
@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; }
可以看到生成代理類的代碼非常的簡短,主要就是遍歷我們的mProxyMap,然后取得每一個ProxyInfo,***通過mFileUtils.createSourceFile來創(chuàng)建文件對象,類名為proxyInfo.getProxyClassFullName(),寫入的內(nèi)容為proxyInfo.generateJavaCode().
看來生成Java代碼的方法都在ProxyInfo里面。
c.生成Java代碼
這里我們主要關(guān)注其生成Java代碼的方式。
下面主要看生成Java代碼的方法:
#ProxyInfo //key為id,value為對應(yīng)的成員變量 public MapmInjectElements = new HashMap (); 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"); }
這里主要就是靠收集到的信息,拼接完成的代理類對象了,看起來會比較頭疼,不過我給出一個生成后的代碼,對比著看會很多。
package com.zhy.ioc_sample; import com.zhy.ioc.*; public class MainActivity$$ViewInjector implements ViewInjector{ @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)); } } }
這樣對著上面代碼看會好很多,其實(shí)就死根據(jù)收集到的成員變量(通過@BindView聲明的),然后根據(jù)我們具體要實(shí)現(xiàn)的需求去生成java代碼。
這里注意下,生成的代碼實(shí)現(xiàn)了一個接口ViewInjector,該接口是為了統(tǒng)一所有的代理類對象的類型,到時(shí)候我們需要強(qiáng)轉(zhuǎn)代理類對象為該接口類型,調(diào)用其方法;接口是泛型,主要就是傳入實(shí)際類對象,例如MainActivity,因?yàn)槲覀冊谏纱眍愔械拇a,實(shí)際上就是實(shí)際類.成員變量的方式進(jìn)行訪問,所以,使用編譯時(shí)注解的成員變量一般都不允許private修飾符修飾(有的允許,但是需要提供getter,setter訪問方法)。
這里采用了完全拼接的方式編寫Java代碼,你也可以使用一些開源庫,來通過Java api的方式來生成代碼,例如:javapoet.
A Java API for generating .java source files.
到這里我們就完成了代理類的生成,這里任何的注解處理器的編寫方式基本都遵循著收集信息、生成代理類的步驟。
五、API模塊的實(shí)現(xiàn)
有了代理類之后,我們一般還會提供API供用戶去訪問,例如本例的訪問入口是
//Activity中 Ioc.inject(Activity); //Fragment中,獲取ViewHolder中 Ioc.inject(this, view);
模仿了butterknife,***個參數(shù)為宿主對象,第二個參數(shù)為實(shí)際調(diào)用findViewById的對象;當(dāng)然在Actiivty中,兩個參數(shù)就一樣了。
API一般如何編寫呢?
其實(shí)很簡單,只要你了解了其原理,這個API就干兩件事:
根據(jù)傳入的host尋找我們生成的代理類:例如MainActivity->MainActity$$ViewInjector。
強(qiáng)轉(zhuǎn)為統(tǒng)一的接口,調(diào)用接口提供的方法。
這兩件事應(yīng)該不復(fù)雜,***件事是拼接代理類名,然后反射生成對象,第二件事強(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{ void inject(T t , Object object); }
代碼很簡單,拼接代理類的全路徑,然后通過newInstance生成實(shí)例,然后強(qiáng)轉(zhuǎn),調(diào)用代理類的inject方法。
這里一般情況會對生成的代理類做一下緩存處理,比如使用Map存儲下,沒有再生成,這里我們就不去做了。
這樣我們就完成了一個編譯時(shí)注解框架的編寫。
以上是“Android怎么編寫基于編譯時(shí)注解的項(xiàng)目”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習(xí)更多知識,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道!