1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        手把手教你實(shí)現(xiàn)Android編譯期注解

        共 4109字,需瀏覽 9分鐘

         ·

        2021-10-15 16:31

        作者:vivo互聯(lián)網(wǎng)客戶端團(tuán)隊(duì)-Wu Yue


        一、編譯期注解在開發(fā)中的重要性


        從早期令人驚艷的ButterKnife,到后來的以ARouter為首的各種路由框架,再到現(xiàn)在谷歌大力推行的Jetpack組件,越來越多的第三方框架都在使用編譯期注解這門技術(shù),可以說不管你是想要深入研究這些第三方框架的原理 還是要成為一個(gè)Android高級(jí)開發(fā)工程師,編譯期注解都是你不得不好好掌握的一門基礎(chǔ)技術(shù)。


        本文從基礎(chǔ)的運(yùn)行期注解用法開始,逐步演進(jìn)到編譯期注解的用法,讓你真正明白編譯期注解到底應(yīng)該在什么場景下使用,怎么用,用了有哪些好處。


        二、手寫運(yùn)行期注解


        類似下面這種寫法,當(dāng)View一多得不停的findViewById 寫很多行,手寫起來很麻煩,我們首先嘗試用運(yùn)行期注解來解決這個(gè)問題,看看能不能自動(dòng)處理這些findViewById的操作。



        首先是工程結(jié)構(gòu),肯定要定義一個(gè)lib module。



        其次定義我們的注解類:



        有了這個(gè)注解的類,我們就可以在我們的MainAcitivity先用起來,雖然此時(shí)這個(gè)注解還并未起到什么作用。



        到這里要稍微想一下,此時(shí)我們要做的是 通過注解來將R.id.xx 賦值給對應(yīng)的field,也就是你定義的那些view對象(例如紅框中的tv),對于我們的lib工程來說,因?yàn)槭荕ainActivity 要依賴lib,自然你lib不可以依賴Main所屬的app工程了,這里有2個(gè)原因:


        • ?A依賴B ,B依賴A的循環(huán)依賴是肯定會(huì)報(bào)錯(cuò)的;

        • 既然你要做一個(gè)lib 那你肯定不能依賴使用者的宿主 否則怎么能叫l(wèi)ib呢?


        所以這個(gè)問題就變成了,lib工程 只能拿到Acitivty,拿不到宿主的MainActivity , 既然拿不到宿主的MainActivity,那我怎么知道這個(gè)activity有多少個(gè)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)用一下這個(gè)方法 即可:



        到這里其實(shí)有人就要問了,這個(gè)運(yùn)行時(shí)注解看起來也不難啊,為啥好像用的人不是很多?問題就出在剛才反射的那堆方法里,反射大家都知道 會(huì)對Android運(yùn)行時(shí)帶來一些性能損耗,而這里的代碼是一段循環(huán), 也就是說這里的代碼會(huì)隨著你使用lib的Activity的界面復(fù)雜程度的提高 而變得越來越慢,這是一個(gè)會(huì)隨著你界面復(fù)雜度提高而逐步劣化的過程,?單次反射對于今天的手機(jī)來說幾乎已經(jīng)不存在什么性能消耗了,但是這種for循環(huán)中使用反射還是盡量少用。


        三、手寫編譯期注解


        為了解決這個(gè)問題,就要使用編譯期注解?,F(xiàn)在我們來嘗試用編譯期注解來解決上述的問題。前面我們說過,運(yùn)行期注解可以用反射來拿到宿主的field 從而完成需求,為了解決反射的性能問題,我們其實(shí)想要的代碼是這樣的:


        我們可以在app 的module 中新建一個(gè)MainActivityViewBinding的類:



        然后在我們的BindingView(注意我們的BindingView是在lib module下的)中來調(diào)用這個(gè)方法不就解決這個(gè)反射的問題了嗎?



        但是這里會(huì)有個(gè)問題 就是你既然是一個(gè)lib 你不能依賴宿主 ,所以在lib Module 中你其實(shí)拿不到 MainActivityViewBinding 這個(gè)類的,還是得利用反射。



        可以看一下上面注釋掉的代碼,為啥不直接字符串寫死?因?yàn)槟闶莑ib庫你當(dāng)然得是動(dòng)態(tài)的,不然怎么給別人用?其實(shí)就是獲取宿主的class名稱然后加上一個(gè)固定的后綴ViewBinding 即可。這個(gè)時(shí)候 我們就拿到這個(gè)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();        }    }}


        看下此時(shí)的代碼結(jié)構(gòu):



        有人這里要問,這里你不還是用了反射么,對! 這里雖然用了反射,但是我這里的反射只會(huì)調(diào)用一次,不管你的activity有都少field,在我這里反射方法都只會(huì)執(zhí)行一次。所以性能肯定是比之前的方案要快很多倍的。接著看,雖然此刻代碼可以正常運(yùn)行,但是還有一個(gè)問題, 雖然我可以在lib中調(diào)用到我們app宿主的類的構(gòu)造方法,但是,宿主的這個(gè)類依舊是我們手寫的啊?那你這個(gè)lib庫 還是沒有起到任何可以讓我們少寫代碼的作用。


        這個(gè)時(shí)候就需要我們的apt 出場了,也就是編譯期注解的核心部分了。我們創(chuàng)建一個(gè)Java Library,注意是Java lib不是android lib,然后在app module中引入他。


        注意 引入的方式 不是imp了,是annotation processor ;



        然后我們來修改一下lib_processor,首先創(chuàng)建一個(gè) 注解處理類:



        再創(chuàng)建文件resources/META-INF/

        services/javax.annotation.processing.Processor ,這里要注意 文件夾創(chuàng)建不要寫錯(cuò)了。



        然后再這個(gè)Processor指定 一下我們的注解處理器即可:



        到這里還沒完,我們得告訴這個(gè)注解處理器 只處理我們的BindView注解即可,否則這個(gè)注解處理器默認(rèn)處理全部注解 速度就太慢了,但是此時(shí) 我們的BindView這個(gè)注解類還在lib倉里面,顯然我們要調(diào)整一下我們的工程結(jié)構(gòu):



        我們再新建一個(gè)Javalib,只放BindView即可,然后讓我們的lib_processor和app 都依賴這個(gè)lib_interface即可。再稍微修改一下代碼,此時(shí)我們是編譯期處理,所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 set, RoundEnvironment roundEnvironment) {        return false;    }     //要支持哪些注解    @Override    public Set getSupportedAnnotationTypes() {        return Collections.singleton(BindView.class.getCanonicalName());    }}


        到此我們的大部分工作就處理完畢了。再看一下代碼結(jié)構(gòu)(這里的代碼結(jié)構(gòu)一定要理解清楚為什么這樣設(shè)計(jì),否則你是學(xué)不會(huì)編譯期注解的)。



        我們現(xiàn)在已經(jīng)能夠做到 通過 lib 這個(gè)sdk 調(diào)用到MainActivityViewBinding這個(gè)里面的方法,但是他 還在app倉是我們手寫的,不太智能,還沒辦法用。我們需要在注解處理器里面 ,動(dòng)態(tài)的生成這個(gè)類,只要能完成這個(gè)步驟,那我們的SDK也就基本完成了。


        這里要提一下,很多人注解始終學(xué)不會(huì)就是卡在這里,因?yàn)樘嗟奈恼禄蛘呓坛躺蟻砭褪荍avapoet 那一套代碼,壓根學(xué)不會(huì),或者只能復(fù)制粘貼別人的東西,稍微變動(dòng)一下就不會(huì)了,其實(shí)這里最佳的學(xué)習(xí)方式是先用StringBuffer 字符串拼接的方式 拼出我們想要的代碼就可以了,通過這個(gè)字符串拼接的過程 來理解對應(yīng)的api以及生成java代碼的思路,然后最后再用JavaPoet來優(yōu)化代碼即可。


        我們可以先思考一下, 如果用字符串拼接的方式來做這個(gè)生成類的操作要完成哪些步驟。


        • 首先要獲取哪些類使用了我們的BindView注解;

        • 獲取這些類中使用了BindView注解的field以及他們對應(yīng)的值;

        • 拿到這些類的類名稱以便我們生成諸如MainActivityViewBinding這樣的類名;

        • 拿到這些類的包名,因?yàn)槲覀兩傻念愐妥⒔馑鶎俚念悓儆谕粋€(gè)package 才不會(huì)出現(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ù)校驗(yàn)可以用這個(gè)來提示用戶你哪里寫的不對        messager.printMessage(Diagnostic.Kind.NOTE, " BindingProcessor init");        super.init(processingEnvironment);    }     private void generateCodeByStringBuffer(String className, List elements) throws IOException {         //你要生成的類 要和 注解的類 同屬一個(gè)package 所以還要取 package的名稱        String packageName = elementUtils.getPackageOf(elements.get(0)).getQualifiedName().toString();        StringBuffer sb = new StringBuffer();        // 每個(gè)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(Setextends TypeElement> set, RoundEnvironment roundEnvironment) {         // key 就是使用注解的class的類名 element就是使用注解本身的元素 一個(gè)class 可以有多個(gè)使用注解的field        Map<String, List> fieldMap = new HashMap<>();        // 這里 獲取到 所有使用了 BindView 注解的 element        for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {            //取到 這個(gè)注解所屬的class的Name            String className = element.getEnclosingElement().getSimpleName().toString();            //取到值以后 判斷map中 有沒有 如果沒有就直接put 有的話 就直接在這個(gè)value中增加一個(gè)element            if (fieldMap.get(className) != null) {                List elementList = fieldMap.get(className);                elementList.add(element);            } else {                List elements = new ArrayList<>();                elements.add(element);                fieldMap.put(className, elements);            }        }         //遍歷map,開始生成輔助類        for (Map.Entry<String, List> 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());    }}


        最后看下效果:



        雖然生成的代碼格式不太好看,但是運(yùn)行起來是ok的。這里要注意一下Element 這個(gè)接口,實(shí)際上使用編譯期注解的時(shí)候 如果能夠理解了Element,那后續(xù)的工作就簡單不少。



        主要關(guān)注Element的這5個(gè)子類即可,舉個(gè)例子:

        package com.smart.annotationlib_2;//PackageElement |表示一個(gè)包程序元素//  TypeElement 表示一個(gè)類或接口程序元素。public class VivoTest {    //VariableElement |表示一個(gè)字段、enum 常量、方法或構(gòu)造方法參數(shù)、局部變量或異常參數(shù)。    int a;     //VivoTest 這個(gè)方法 :ExecutableElement|表示某個(gè)類或接口的方法、構(gòu)造方法或初始化程序(靜態(tài)或?qū)嵗ㄗ⑨岊愋驮亍?/span>    //int b 這個(gè)函數(shù)參數(shù): TypeParameterElement |表示一般類、接口、方法或構(gòu)造方法元素的形式類型參數(shù)。    public VivoTest(int b ) {        this.a = b;    }}


        四、Javapoet生成代碼


        有了上面的基礎(chǔ) 再用 Javapoet 寫一遍字符串拼接來生成java代碼的過程, 就不會(huì)難以理解了。


        private void generateCodeByJavapoet(String className, List 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代碼。有興趣的可以嘗試一下。


        五、編譯期注解的總結(jié)


        首先是大家關(guān)注的性能方面,對于運(yùn)行時(shí)注解來說,會(huì)產(chǎn)生大量的反射代碼,而且反射調(diào)用的次數(shù)會(huì)隨著項(xiàng)目復(fù)雜度的提高而變的越來越多,是一個(gè)逐步劣化的過程,而對于編譯期注解來說,反射的調(diào)用次數(shù)是固定的,他并不會(huì)隨著項(xiàng)目復(fù)雜度的提高而變的性能越來越差,實(shí)際上對于大多數(shù)運(yùn)行時(shí)注解的項(xiàng)目都可以通過編譯期注解來大幅提高框架的性能,比如著名的Dagger、EventBus 等等,他們的首個(gè)版本都是運(yùn)行時(shí)注解,后續(xù)版本都統(tǒng)一替換成了編譯期注解。


        其次回顧一下前面我們編譯期注解的開發(fā)流程以后,可以得出以下幾點(diǎn)結(jié)論:


        • 編譯期注解只能生成代碼,但是不能修改代碼;

        • 注解生成的代碼 必須要手動(dòng)被調(diào)用,他自己是不會(huì)被調(diào)用的;

        • 對于SDK的編寫者來說,即使是編譯期注解,往往也免不了至少要走一次反射,而反射的作用主要就是調(diào)用你注解處理器生成的代碼。


        這里可能會(huì)有小伙伴問,既然編譯期注解只能生成代碼不能修改代碼,那作用很有限啊,為啥不直接用類似于ASM?、Javassist 等字節(jié)碼工具呢,這些工具不但可以生成代碼而且還可以修改代碼,功能更強(qiáng)勁。因?yàn)檫@些字節(jié)碼工具生成的直接是class,且寫法復(fù)雜容易出錯(cuò),也不易于調(diào)試,小規(guī)模寫一下類似于防止快速點(diǎn)擊之類的東西還可以,大規(guī)模開發(fā)第三方框架其實(shí)也挺不方便的,遠(yuǎn)遠(yuǎn)不如編譯期注解來的效率高。


        此外,再仔細(xì)想想,我們前文中提到的編譯期注解的寫法做成第三方庫給別人使用以后,還是需要使用者手動(dòng)的在合適的時(shí)機(jī)調(diào)用一下 “init” 方法的,但是有些出色的第三方庫可以做到連init方法都不需要使用者手動(dòng)調(diào)用了,使用起來非常方便,這又是怎么做到的?其實(shí)也不難,多數(shù)情況都是這些第三方庫用編譯期注解生成了代碼以后,再配合ASM等字節(jié)碼工具直接幫你調(diào)用了init方法 ,從而讓你免去手動(dòng)調(diào)用的過程。核心仍舊是編譯期注解,只不過是用字節(jié)碼工具省略了一步而已。


        瀏覽 66
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            青娱乐黄色视频 | 国产精品99久久久久久www | 日韩在线1 | 天美一区 | 国产激情123区 | 日本十九禁免费观看视频 | 韩国日本在线视频 | 又黄又粗a毛片一级 | 女生张开腿让男生捅 | 亚洲中文无码AV在线 |