您好,登錄后才能下訂單哦!
本篇文章為大家展示了Android中怎么實現(xiàn)編譯期注解,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
從早期令人驚艷的ButterKnife,到后來的以ARouter為首的各種路由框架,再到現(xiàn)在谷歌大力推行的Jetpack組件,越來越多的第三方框架都在使用編譯期注解這門技術(shù),可以說不管你是想要深入研究這些第三方框架的原理 還是要成為一個Android高級開發(fā)工程師,編譯期注解都是你不得不好好掌握的一門基礎(chǔ)技術(shù)。
本文從基礎(chǔ)的運行期注解用法開始,逐步演進(jìn)到編譯期注解的用法,讓你真正明白編譯期注解到底應(yīng)該在什么場景下使用,怎么用,用了有哪些好處。
類似下面這種寫法,當(dāng)View一多得不停的findViewById 寫很多行,手寫起來很麻煩,我們首先嘗試用運行期注解來解決這個問題,看看能不能自動處理這些findViewById的操作。
首先是工程結(jié)構(gòu),肯定要定義一個lib module。
其次定義我們的注解類:
有了這個注解的類,我們就可以在我們的MainAcitivity先用起來,雖然此時這個注解還并未起到什么作用。
到這里要稍微想一下,此時我們要做的是 通過注解來將R.id.xx 賦值給對應(yīng)的field,也就是你定義的那些view對象(例如紅框中的tv),對于我們的lib工程來說,因為是MainActivity 要依賴lib,自然你lib不可以依賴Main所屬的app工程了,這里有2個原因:
A依賴B ,B依賴A的循環(huán)依賴是肯定會報錯的;
既然你要做一個lib 那你肯定不能依賴使用者的宿主 否則怎么能叫l(wèi)ib呢?
所以這個問題就變成了,lib工程 只能拿到Acitivty,拿不到宿主的MainActivity , 既然拿不到宿主的MainActivity,那我怎么知道這個activity有多少個field?這里就要用到反射了。
public class BindingView { public static void init(Activity activity) { Field[] fields = activity.getClass().getDeclaredFields(); for (Field field : fields) { //獲取 被注解 BindView annotation = field.getAnnotation(BindView.class); if (annotation != null) { int viewId = annotation.value(); field.setAccessible(true); try { field.set(activity, activity.findViewById(viewId)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } }
最后我們在宿主的MainActivity中調(diào)用一下這個方法 即可:
到這里其實有人就要問了,這個運行時注解看起來也不難啊,為啥好像用的人不是很多?問題就出在剛才反射的那堆方法里,反射大家都知道 會對Android運行時帶來一些性能損耗,而這里的代碼是一段循環(huán), 也就是說這里的代碼會隨著你使用lib的Activity的界面復(fù)雜程度的提高 而變得越來越慢,這是一個會隨著你界面復(fù)雜度提高而逐步劣化的過程, 單次反射對于今天的手機來說幾乎已經(jīng)不存在什么性能消耗了,但是這種for循環(huán)中使用反射還是盡量少用。
為了解決這個問題,就要使用編譯期注解。現(xiàn)在我們來嘗試用編譯期注解來解決上述的問題。前面我們說過,運行期注解可以用反射來拿到宿主的field 從而完成需求,為了解決反射的性能問題,我們其實想要的代碼是這樣的:
我們可以在app 的module 中新建一個MainActivityViewBinding的類:
然后在我們的BindingView(注意我們的BindingView是在lib module下的)中來調(diào)用這個方法不就解決這個反射的問題了嗎?
但是這里會有個問題 就是你既然是一個lib 你不能依賴宿主 ,所以在lib Module 中你其實拿不到 MainActivityViewBinding 這個類的,還是得利用反射。
可以看一下上面注釋掉的代碼,為啥不直接字符串寫死?因為你是lib庫你當(dāng)然得是動態(tài)的,不然怎么給別人用?其實就是獲取宿主的class名稱然后加上一個固定的后綴ViewBinding 即可。這個時候 我們就拿到這個Binding的class了,對吧,剩下就是調(diào)用構(gòu)造方法即可。
public class BindingView { public static void init(Activity activity) { try { Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "ViewBinding"); Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass()); constructor.newInstance(activity); } catch (ClassNotFoundException | NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
看下此時的代碼結(jié)構(gòu):
有人這里要問,這里你不還是用了反射么,對! 這里雖然用了反射,但是我這里的反射只會調(diào)用一次,不管你的activity有都少field,在我這里反射方法都只會執(zhí)行一次。所以性能肯定是比之前的方案要快很多倍的。接著看,雖然此刻代碼可以正常運行,但是還有一個問題, 雖然我可以在lib中調(diào)用到我們app宿主的類的構(gòu)造方法,但是,宿主的這個類依舊是我們手寫的?。磕悄氵@個lib庫 還是沒有起到任何可以讓我們少寫代碼的作用。
這個時候就需要我們的apt 出場了,也就是編譯期注解的核心部分了。我們創(chuàng)建一個Java Library,注意是Java lib不是android lib,然后在app module中引入他。
注意 引入的方式 不是imp了,是annotation processor ;
然后我們來修改一下lib_processor,首先創(chuàng)建一個 注解處理類:
再創(chuàng)建文件resources/META-INF/services/javax.annotation.processing.Processor ,這里要注意 文件夾創(chuàng)建不要寫錯了。
然后再這個Processor指定 一下我們的注解處理器即可:
到這里還沒完,我們得告訴這個注解處理器 只處理我們的BindView注解即可,否則這個注解處理器默認(rèn)處理全部注解 速度就太慢了,但是此時 我們的BindView這個注解類還在lib倉里面,顯然我們要調(diào)整一下我們的工程結(jié)構(gòu):
我們再新建一個Javalib,只放BindView即可,然后讓我們的lib_processor和app 都依賴這個lib_interface即可。再稍微修改一下代碼,此時我們是編譯期處理,所Policy不用是runtime了。
@Retention(RetentionPolicy.SOURCE) @Target(ElementType.FIELD) public @interface BindView { int value(); } public class BindingProcessor extends AbstractProcessor { Messager messager; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { messager = processingEnvironment.getMessager(); messager.printMessage(Diagnostic.Kind.NOTE, " BindingProcessor init"); super.init(processingEnvironment); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { return false; } //要支持哪些注解 @Override public Set<String> getSupportedAnnotationTypes() { return Collections.singleton(BindView.class.getCanonicalName()); } }
到此我們的大部分工作就處理完畢了。再看一下代碼結(jié)構(gòu)(這里的代碼結(jié)構(gòu)一定要理解清楚為什么這樣設(shè)計,否則你是學(xué)不會編譯期注解的)。
我們現(xiàn)在已經(jīng)能夠做到 通過 lib 這個sdk 調(diào)用到MainActivityViewBinding這個里面的方法,但是他 還在app倉是我們手寫的,不太智能,還沒辦法用。我們需要在注解處理器里面 ,動態(tài)的生成這個類,只要能完成這個步驟,那我們的SDK也就基本完成了。
這里要提一下,很多人注解始終學(xué)不會就是卡在這里,因為太多的文章或者教程上來就是Javapoet 那一套代碼,壓根學(xué)不會,或者只能復(fù)制粘貼別人的東西,稍微變動一下就不會了,其實這里最佳的學(xué)習(xí)方式是先用StringBuffer 字符串拼接的方式 拼出我們想要的代碼就可以了,通過這個字符串拼接的過程 來理解對應(yīng)的api以及生成java代碼的思路,然后最后再用JavaPoet來優(yōu)化代碼即可。
我們可以先思考一下, 如果用字符串拼接的方式來做這個生成類的操作要完成哪些步驟。
首先要獲取哪些類使用了我們的BindView注解;
獲取這些類中使用了BindView注解的field以及他們對應(yīng)的值;
拿到這些類的類名稱以便我們生成諸如MainActivityViewBinding這樣的類名;
拿到這些類的包名,因為我們生成的類要和注解所屬的類屬于同一個package 才不會出現(xiàn)field 訪問權(quán)限的問題;
上述條件都具備以后 就用字符串拼接的方式 拼接出我們想要的java代碼 即可。
這里就直接上代碼了,重要部分 直接看注釋即可,有了上面的步驟分析再看代碼注釋應(yīng)該不難理解。
public class BindingProcessor extends AbstractProcessor { Messager messager; Filer filer; Elements elementUtils; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { //主要是輸出一些重要的日志使用 messager = processingEnvironment.getMessager(); //你就理解成最終我們寫java文件 要用到的重要 輸出參數(shù)即可 filer = processingEnvironment.getFiler(); //一些方便的utils方法 elementUtils = processingEnvironment.getElementUtils(); //這里要注意的是Diagnostic.Kind.ERROR 是可以讓編譯失敗的 一些重要的參數(shù)校驗可以用這個來提示用戶你哪里寫的不對 messager.printMessage(Diagnostic.Kind.NOTE, " BindingProcessor init"); super.init(processingEnvironment); } private void generateCodeByStringBuffer(String className, List<Element> elements) throws IOException { //你要生成的類 要和 注解的類 同屬一個package 所以還要取 package的名稱 String packageName = elementUtils.getPackageOf(elements.get(0)).getQualifiedName().toString(); StringBuffer sb = new StringBuffer(); // 每個java類 的開頭都是package sth... sb.append("package "); sb.append(packageName); sb.append(";\n"); // public class XXXActivityViewBinding { final String classDefine = "public class " + className + "ViewBinding { \n"; sb.append(classDefine); //定義構(gòu)造函數(shù)的開頭 String constructorName = "public " + className + "ViewBinding(" + className + " activity){ \n"; sb.append(constructorName); //遍歷所有element 生成諸如 activity.tv=activity.findViewById(R.id.xxx) 之類的語句 for (Element e : elements) { sb.append("activity." + e.getSimpleName() + "=activity.findViewById(" + e.getAnnotation(BindView.class).value() + ");\n"); } sb.append("\n}"); sb.append("\n }"); //文件內(nèi)容確定以后 直接生成即可 JavaFileObject sourceFile = filer.createSourceFile(className + "ViewBinding"); Writer writer = sourceFile.openWriter(); writer.write(sb.toString()); writer.close(); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { // key 就是使用注解的class的類名 element就是使用注解本身的元素 一個class 可以有多個使用注解的field Map<String, List<Element>> fieldMap = new HashMap<>(); // 這里 獲取到 所有使用了 BindView 注解的 element for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) { //取到 這個注解所屬的class的Name String className = element.getEnclosingElement().getSimpleName().toString(); //取到值以后 判斷map中 有沒有 如果沒有就直接put 有的話 就直接在這個value中增加一個element if (fieldMap.get(className) != null) { List<Element> elementList = fieldMap.get(className); elementList.add(element); } else { List<Element> elements = new ArrayList<>(); elements.add(element); fieldMap.put(className, elements); } } //遍歷map,開始生成輔助類 for (Map.Entry<String, List<Element>> entry : fieldMap.entrySet()) { try { generateCodeByStringBuffer(entry.getKey(), entry.getValue()); } catch (IOException e) { e.printStackTrace(); } } return false; } //要支持哪些注解 @Override public Set<String> getSupportedAnnotationTypes() { return Collections.singleton(BindView.class.getCanonicalName()); } }
最后看下效果:
雖然生成的代碼格式不太好看,但是運行起來是ok的。這里要注意一下Element 這個接口,實際上使用編譯期注解的時候 如果能夠理解了Element,那后續(xù)的工作就簡單不少。
主要關(guān)注Element的這5個子類即可,舉個例子:
package com.smart.annotationlib_2;//PackageElement |表示一個包程序元素 // TypeElement 表示一個類或接口程序元素。 public class VivoTest { //VariableElement |表示一個字段、enum 常量、方法或構(gòu)造方法參數(shù)、局部變量或異常參數(shù)。 int a; //VivoTest 這個方法 :ExecutableElement|表示某個類或接口的方法、構(gòu)造方法或初始化程序(靜態(tài)或?qū)嵗?,包括注釋類型元素? //int b 這個函數(shù)參數(shù): TypeParameterElement |表示一般類、接口、方法或構(gòu)造方法元素的形式類型參數(shù)。 public VivoTest(int b ) { this.a = b; } }
有了上面的基礎(chǔ) 再用 Javapoet 寫一遍字符串拼接來生成java代碼的過程, 就不會難以理解了。
private void generateCodeByJavapoet(String className, List<Element> elements) throws IOException { //聲明構(gòu)造方法 MethodSpec.Builder constructMethodBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addParameter(ClassName.bestGuess(className), "activity"); //構(gòu)造方法里面 增加語句 for (Element e : elements) { constructMethodBuilder.addStatement("activity." + e.getSimpleName() + "=activity.findViewById(" + e.getAnnotation(BindView.class).value() + ");"); } //聲明類 TypeSpec viewBindingClass = TypeSpec.classBuilder(className + "ViewBinding").addModifiers(Modifier.PUBLIC).addMethod(constructMethodBuilder.build()).build(); String packageName = elementUtils.getPackageOf(elements.get(0)).getQualifiedName().toString(); JavaFile build = JavaFile.builder(packageName, viewBindingClass).build(); build.writeTo(filer); }
這里要提一下,現(xiàn)在越來越多的人使用Kotlin語言開發(fā)app,你甚至可以使用https://github.com/square/kotlinpoet 來直接生成Kotlin代碼。有興趣的可以嘗試一下。
首先是大家關(guān)注的性能方面,對于運行時注解來說,會產(chǎn)生大量的反射代碼,而且反射調(diào)用的次數(shù)會隨著項目復(fù)雜度的提高而變的越來越多,是一個逐步劣化的過程,而對于編譯期注解來說,反射的調(diào)用次數(shù)是固定的,他并不會隨著項目復(fù)雜度的提高而變的性能越來越差,實際上對于大多數(shù)運行時注解的項目都可以通過編譯期注解來大幅提高框架的性能,比如著名的Dagger、EventBus 等等,他們的首個版本都是運行時注解,后續(xù)版本都統(tǒng)一替換成了編譯期注解。
其次回顧一下前面我們編譯期注解的開發(fā)流程以后,可以得出以下幾點結(jié)論:
編譯期注解只能生成代碼,但是不能修改代碼;
注解生成的代碼 必須要手動被調(diào)用,他自己是不會被調(diào)用的;
對于SDK的編寫者來說,即使是編譯期注解,往往也免不了至少要走一次反射,而反射的作用主要就是調(diào)用你注解處理器生成的代碼。
這里可能會有小伙伴問,既然編譯期注解只能生成代碼不能修改代碼,那作用很有限啊,為啥不直接用類似于ASM 、Javassist 等字節(jié)碼工具呢,這些工具不但可以生成代碼而且還可以修改代碼,功能更強勁。因為這些字節(jié)碼工具生成的直接是class,且寫法復(fù)雜容易出錯,也不易于調(diào)試,小規(guī)模寫一下類似于防止快速點擊之類的東西還可以,大規(guī)模開發(fā)第三方框架其實也挺不方便的,遠(yuǎn)遠(yuǎn)不如編譯期注解來的效率高。
此外,再仔細(xì)想想,我們前文中提到的編譯期注解的寫法做成第三方庫給別人使用以后,還是需要使用者手動的在合適的時機調(diào)用一下 “init” 方法的,但是有些出色的第三方庫可以做到連init方法都不需要使用者手動調(diào)用了,使用起來非常方便,這又是怎么做到的?其實也不難,多數(shù)情況都是這些第三方庫用編譯期注解生成了代碼以后,再配合ASM等字節(jié)碼工具直接幫你調(diào)用了init方法 ,從而讓你免去手動調(diào)用的過程。核心仍舊是編譯期注解,只不過是用字節(jié)碼工具省略了一步而已。
上述內(nèi)容就是Android中怎么實現(xiàn)編譯期注解,你們學(xué)到知識或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識儲備,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。