編輯:Android資訊
在Android應用開發中,我們常常為了提升開發效率會選擇使用一些基於注解的框架,但是由於反射造成一定運行效率的損耗,所以我們會更青睐於編譯時注解的框架,例如:
類似的庫還有非常多,大多這些的庫都是為了自動幫我們完成日常編碼中需要重復編寫的部分(例如:每個Activity中的View都需要初始化,每個實現Parcelable
接口的對象都需要編寫很多固定寫法的代碼)。
這裡並不是說上述框架就一定沒有使用反射了,其實上述其中部分框架內部還是有部分實現是依賴於反射的,但是很少而且一般都做了緩存的處理,所以相對來說,效率影響很小。
但是在使用這類項目的時候,有時候出現錯誤會難以調試,主要原因還是很多用戶並不了解這類框架其內部的原理,所以遇到問題時會消耗大量的時間去排查。
那麼,於情於理,在編譯時注解框架這麼火的時刻,我們有理由去學習:如何編寫一個機遇編譯時注解的項目
首先,是為了了解其原理,這樣在我們使用類似框架遇到問題的時候,能夠找到正確的途徑去排查問題;其次,我們如果有好的想法,發現某些代碼需要重復創建,我們也可以自己來寫個框架方便自己日常的編碼,提升編碼效率;最後也算是自身技術的提升。
注:以下使用IDE為Android Studio
.
本文將以編寫一個View注入的框架為線索,詳細介紹編寫此類框架的步驟。
在編寫此類框架的時候,一般需要建立多個module,例如本文即將實現的例子:
那麼除了示例以為,一般要建立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
等信息。
注解處理器一般繼承於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
,可以幫助我們去初始化一些父類類:
這裡簡單提一下Elemnet
,我們簡單認識下它的幾個子類,根據下面的注釋,應該已經有了一個簡單認知。
Element - VariableElement //一般代表成員變量 - ExecutableElement //一般代表類中的方法 - TypeElement //一般代表代表類 - PackageElement //一般代表Package
process中的實現,相比較會比較復雜一點,一般你可以認為兩個大步驟:
什麼叫收集信息呢?就是根據你的注解聲明,拿到對應的Element,然後獲取到我們所需要的信息,這個信息肯定是為了後面生成JavaFileObject
所准備的。
例如本例,我們會針對每一個類生成一個代理類,例如MainActivity
我們會生成一個MainActivity$$ViewInjector
。那麼如果多個類中聲明了注解,就對應了多個類,這裡就需要:
ProxyInfo
Map<String, ProxyInfo>
,key為類的全路徑。這裡的描述有點模糊沒關系,一會結合代碼就好理解了。
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。
這樣就完成了信息的收集,收集完成信息後,應該就可以去生成代理類了。
@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; }
可以看到生成代理類的代碼非常的簡短,主要就是遍歷我們的mProxyMap
,然後取得每一個ProxyInfo
,最後通過mFileUtils.createSourceFile
來創建文件對象,類名為proxyInfo.getProxyClassFullName()
,寫入的內容為proxyInfo.generateJavaCode()
.
看來生成Java代碼的方法都在ProxyInfo裡面。
這裡我們主要關注其生成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"); }
這裡主要就是靠收集到的信息,拼接完成的代理類對象了,看起來會比較頭疼,不過我給出一個生成後的代碼,對比著看會很多。
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的方式來生成代碼,例如:javapoet.
A Java API for generating .java source files.
到這裡我們就完成了代理類的生成,這裡任何的注解處理器的編寫方式基本都遵循著收集信息、生成代理類的步驟。
有了代理類之後,我們一般還會提供API供用戶去訪問,例如本例的訪問入口是
//Activity中 Ioc.inject(Activity); //Fragment中,獲取ViewHolder中 Ioc.inject(this, view);
模仿了butterknife,第一個參數為宿主對象,第二個參數為實際調用findViewById
的對象;當然在Actiivty中,兩個參數就一樣了。
API一般如何編寫呢?
其實很簡單,只要你了解了其原理,這個API就干兩件事:
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模塊的編寫。通過文本的學習應該能夠了解基於編譯時注解這類框架運行的原理,以及自己如何去編寫這樣一類框架。
源碼地址: https://github.com/hymanAndroid/ioc-apt-sample
在開發Android應用時,保存數據有這麼幾個方式, 一個是本地保存,一個是放在後台(提供API接口),還有一個是放在開放雲服務上(如 SyncAdapter 會
Android編程中一個共同的困難就是協調Activity的生命周期和長時間運行的任務(task),並且要避免可能的內存洩露。思考下面Activity的代碼,在它
Android studio不僅允許你為你的app和依賴庫創建模塊,同時也可為Android wear,Android TV,Google App Engine等
在Google的廣大支持下,便捷開發Android程序的Native工具層出不窮。其實Android開發涉及到的范圍也不小,一些Web工具有時候也會帶來事半功倍的