流行框架源碼分析(3)-編譯期注解的使用例子

主目錄見:Android高級進(jìn)階知識(這是總目錄索引)
?我們在開發(fā)的時候為了提高效率往往會選擇一個基于注解的框架,但是有時使用反射通常被認(rèn)為是性能的收割機(jī),所以我們會青睞編譯期注解的使用讨跟,其實早在前面我們分析了[EventBus3.0源碼解析]中我們就有看到,還有我們接下來要講的ButterKnife也會用到,當(dāng)然我今天要用來講的例子[LRouter]這個項目也會使用這個。

一.目標(biāo)

?現(xiàn)在編譯期注解這么火熱趣苏,我們有理由要去學(xué)習(xí)一下,我們今天這篇文章是為了掃盲一下梯轻,使得我們下面分析這類型的框架更加得心應(yīng)手食磕。所以今天目標(biāo)是:
1.自己能編寫一個編譯期注解項目;
2.在實際開發(fā)中能使用到這項技術(shù)喳挑;
3.能學(xué)習(xí)此類型的框架的源碼,然后收為己用彬伦。

二.例子編寫

在編寫這個框架之前,我們需要一些準(zhǔn)備伊诵,因為此類框架需要的模塊有點多:


目錄結(jié)構(gòu)

因為這是個完整的項目单绑,我們只是挑出其中的編譯期注解部分來講:

  • lrouter-annotation:用于放注解部分,是個java的模塊
  • lrouter-compiler:用于編寫注解處理器曹宴,是個java模塊
  • lrouter-api:用于提供用戶使用的api的搂橙,是個android模塊
  • app:這個是使用的實例,注解會在這里使用笛坦,也是android模塊

當(dāng)然了区转,目錄不一定就是要分這么多個,大家可以根據(jù)自己的需要版扩,有時候還可以進(jìn)行合并废离。當(dāng)然這些模塊之間是有模塊依賴的:

lrouter-compiler依賴lrouter-annotation模塊

在使用這個注解和api的時候,我們也要添加一些依賴:

lrouter-api依賴lrouter-annotation礁芦,app依賴lrouter-api和lrouter-annotation

當(dāng)然這里的lrouter-api在這里我們使用到的也不多蜻韭,因為這是項目會使用到的api,具體使用可以查看github上面的說明。

1.注解模塊lrouter-annotation實現(xiàn)

注解模塊就是單純地放一些注解湘捎,沒有其他的東西:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Action {
    String name();
    String provider();
}

我們這里舉其中一個注解來看看诀豁,因為我們是編譯期的注解所以我們設(shè)置保留策略為CLASS即編譯期注解(當(dāng)然如果用反射的話Runtime或者還有一個source用的不多),并且說明我們注解是使用在類上面(還可以說明在Field或者M(jìn)ethod等待上面)窥妇,然后這里因為需要一個name和一個provider其中的值為String類型舷胜。

你如果需要幾個注解就跟這個一樣,只要設(shè)置上響應(yīng)的Target和Retention即可活翩。

2.注解處理器lrouter-compiler的實現(xiàn)

這塊內(nèi)容應(yīng)該是編譯期注解的核心了吧烹骨,不過放心,也不會太難材泄,步驟是很固定的沮焕,在這里我們會使用一個auto-service庫以及后面用來生成代碼的javapoet:

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':lrouter-annotation')
}

auto-service可以幫我們?nèi)ド蒑ETA-INF信息:

META-INF信息

接著我們就可以編寫注解處理器的核心代碼了。

2.1.ProviderActionProcessor實現(xiàn)

要實現(xiàn)注解處理器我們要實現(xiàn)一個類繼承AbstractProcessor:

@AutoService(Processor.class)
public class ProviderActionProcessor extends AbstractProcessor{
}

注解處理器里面包含幾個重要的方法:

init() 
初始化拉宗,得到Elements峦树、Types、Filer等工具類
getSupportedAnnotationTypes() 
描述注解處理器需要處理的注解
getSupportedSourceVersion()
處理器使用的java版本
process() 
掃描分析注解旦事,生成代碼

首先我們會復(fù)寫init()方法魁巩,該方法里面會有ProcessingEnvironment參數(shù),我們可以根據(jù)這個來初始化一些輔助類:

    private Filer mFileUtils;//跟文件相關(guān)的類姐浮,用于生成java源代碼的
    private Elements mElementUtils;//元素相關(guān)的類谷遂,可以理解為獲取代碼中的信息
    private Messager mMessager;//是用來打印日志的跟Logger類似

我們重點要說一下Elements,他有幾個子類卖鲤,我們來說明一下:

public class ClassA { // TypeElement
    private int var_0; // VariableElement
    public ClassA() {} // ExecuteableElement

    public void setA( // ExecuteableElement
            int newA // TypeElement
    ) {
    }
}

我們看到類為TypeElement肾扰,變量為VariableElement,方法為ExecuteableElement蛋逾。到這里我們對Element有個簡單的認(rèn)知集晚,然后我們繼續(xù)重寫getSupportedAnnotationTypes()方法:

 @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationTypes = new LinkedHashSet<>();
        annotationTypes.add(Action.class.getCanonicalName());
        annotationTypes.add(Provider.class.getCanonicalName());
        annotationTypes.add(Service.class.getCanonicalName());
        annotationTypes.add(Application.class.getCanonicalName());
        annotationTypes.add(IntentInterceptor.class.getCanonicalName());
        annotationTypes.add(Interceptor.class.getCanonicalName());
        return annotationTypes;
    }

這里我們把我們支持的注解都添加進(jìn)來,然后我們復(fù)寫另外一個方法getSupportedSourceVersion():

 @Override
    public SourceVersion getSupportedSourceVersion(){
        return SourceVersion.latestSupported();
    }

這個里面我們直接返回我們處理器支持的最新的java版本区匣。接著我們就來實現(xiàn)比較復(fù)雜部分的process方法甩恼。

2.2 process()實現(xiàn)

process方法比較復(fù)雜,因為我們主要的代碼邏輯都在這里面沉颂,一般這個方法的步驟有兩個:
?1.收集信息
?2.生成源代碼
什么叫收集信息呢?就是根據(jù)你的注解聲明悦污,拿到對應(yīng)的Element铸屉,然后獲取到我們所需要的信息,這個信息肯定是為了后面生成JavaFileObject所準(zhǔn)備的切端。

在這個例子中彻坛,我們會針對一個注解生成一個類,例如我們Action在app中使用的時候會生成一個MainAction$$Inject類:


自動生成的代碼

然后我們來看怎樣一步一步生成這個類的。
1)收集信息

   @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mProviderMap.clear();
        mActionMap.clear();
        mServiceMap.clear();
        mApplicaitonMap.clear();
        mIntentInterceptMap.clear();
        mInterceptMap.clear();

        if (!annotations.isEmpty()) {
            Set<? extends Element> elesWithAction = roundEnv.getElementsAnnotatedWith(Action.class);
            Set<? extends Element> elesWithProvider = roundEnv.getElementsAnnotatedWith(Provider.class);
            Set<? extends Element> elesWithService = roundEnv.getElementsAnnotatedWith(Service.class);
            Set<? extends Element> elesWithApplication = roundEnv.getElementsAnnotatedWith(Application.class);
            Set<? extends Element> elesWithIntentInterceptor = roundEnv.getElementsAnnotatedWith(IntentInterceptor.class);
            Set<? extends Element> elesWithInterceptor = roundEnv.getElementsAnnotatedWith(Interceptor.class);
...........
                    return true;
        }
        return false;
    }

我們看到這里會獲取到所有的使用Action等注解的元素集合昌屉,我們看到這里會先調(diào)用clear()方法清除掉map中的數(shù)據(jù)钙蒙,因為process()方法有可能會調(diào)用多次,為了避免生成重復(fù)的源代碼间驮,我們這里做一下清理工作躬厌。獲取到所有的注解的元素集合了我們就可以生成源代碼了。
2)生成代碼

   @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//省略收集信息代碼
 ................
            try {
                generateProviderHelper(elesWithProvider);
                generateActionHelper(elesWithAction);
                generateServiceHelper(elesWithService);
                generateApplicationHelper(elesWithApplication);
                generateIntentInterceptorHelper(elesWithIntentInterceptor);
                generateInterceptorHelper(elesWithInterceptor);
            } catch (Exception e) {
               e.printStackTrace();
            }
            return true;
        }
        return false;
    }

我們看到代碼會分別生成對應(yīng)的注解的源代碼竞帽,我們這里挑一個Action的生成代碼方法來看:

    private void generateActionHelper(Set<? extends Element> elesWithAction) throws Exception{
//遍歷添加了Action注解的元素集合
        for (Element element : elesWithAction){
//因為我們action是放在類上面的注解扛施,所以我們這里會進(jìn)行檢查一下,而且這里類不能為private的
            checkAnnotationValid(element, Action.class);

            TypeElement  classElement = (TypeElement) element;
            //full class name
            String actionClassName = classElement.getQualifiedName().toString();

            ProxyInfo proxyInfo = mActionMap.get(actionClassName);
            if (null == proxyInfo){
//構(gòu)造proxyInfo對象屹篓,里面主要用于存放注解標(biāo)注的類信息與生成類信息
                proxyInfo = new ProxyInfo(mElementUtils, classElement);
                mActionMap.put(actionClassName, proxyInfo);
            }
//同時將action注解里面的name和provider取出設(shè)置給proxyinfo
            Action actionAnnotation = classElement.getAnnotation(Action.class);
            proxyInfo.setName(actionAnnotation.name());
            proxyInfo.setProvider(actionAnnotation.provider());
        }
//有了完整的信息之后就遍歷出來生成源代碼
        for (String key : mActionMap.keySet()) {
            ProxyInfo proxyInfo = mActionMap.get(key);
//這個是用javapoet生成的疙渣,這個是方法的生成
            MethodSpec.Builder initBuilder = MethodSpec.methodBuilder(METHOD_NAME)
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Override.class)
                    .returns(TypeName.VOID);
//這個是方法里面的代碼生成
            initBuilder.addStatement("$T $N = $T.getInstance($T.getInstance()).findProvider($S)",
                    LRProviderClass,LRProviderClass.simpleName().toLowerCase(),LRouterClass,LRouterApplicationClass,proxyInfo.getProvider());
            initBuilder.addCode("if(null != $N){\n",LRProviderClass.simpleName().toLowerCase());
            initBuilder.addCode("$N.registerAction($S,new $T());\n}\n",
                    LRProviderClass.simpleName().toLowerCase(),proxyInfo.getName(),ClassName.get(proxyInfo.typeElement));
//這個是類的生成
            TypeSpec actionInject = TypeSpec.classBuilder(proxyInfo.proxyClassName)
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(InjectorClass)
                    .addMethod(initBuilder.build())
                    .build();
            JavaFile javaFile = JavaFile.builder(PACKAGE_NAME,actionInject).build();
            javaFile.writeTo(mFileUtils);
        }
    }

我們看到代碼比較長,其實邏輯不怎么難堆巧,我們都在關(guān)鍵地方有注釋妄荔,代碼其實就是獲取到使用了注解的類的信息,然后再用javapoet來生成代碼谍肤,關(guān)于[javapoet]我們這里不做詳細(xì)的說明啦租,有興趣可以去這個github上面看看怎么使用。

3.注解在例子中的使用

我們前面編寫了注解和注解生成器谣沸,這樣我們就可以來使用這個注解來生成我們的源代碼了刷钢,使用注解很簡單,跟我們平常是一樣的乳附,我們來看下:

@Action(name = "main",provider = "main")
public class MainAction extends LRAction {//動作的執(zhí)行
}

我們看到注解的使用非常簡單内地,因為這個注解是用在類上面的,所以我們用法如上面所示赋除,當(dāng)然在編寫這個之前我們還要配置一下我們的gradle文件阱缓。首先在工程目錄下的gradle文件下添加:

    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

然后在當(dāng)前模塊下的gradle文件添加:

annotationProcessor project(':lrouter-compiler')

這樣的話,我們只要編譯一下工程举农,就會在當(dāng)前模塊的build——generated——source——apt——debug下生成我們的源代碼了荆针。

4.生成的源碼的使用

生成完源碼了,我們這里在lrouter-api中進(jìn)行使用颁糟,我們這里會用到一點反射航背,跟大家理解的可能不大一樣,不是說我們用到編譯期注解就一點反射不會使用棱貌,這是不正確的玖媚,有可能會我們還是需要用到一點反射,有時我們會用到緩存來減少性能的消耗婚脱。我們現(xiàn)在看lrouter-api下的PackageScanner類:

public class PackageScanner {
    /**
     * 掃描
     * */
    public static List<InjectorPriorityWrapper> scan(Context ctx){
        List<InjectorPriorityWrapper> clazzs = new ArrayList<>();
        try{
            PathClassLoader classLoader = (PathClassLoader) Thread
                    .currentThread().getContextClassLoader();

            DexFile dex = new DexFile(ctx.getPackageResourcePath());
            Enumeration<String> entries = dex.entries();
            while (entries.hasMoreElements()) {
                String entryName = entries.nextElement();
                if (entryName.contains("com.lenovohit")){//過濾掉系統(tǒng)的類
                    Class<?> entryClass = Class.forName(entryName, false,classLoader);
                    if (entryName.contains("Provider$$Inject")){
                        clazzs.add(new InjectorPriorityWrapper(InjectorPriorityWrapper.PROVIDER_PRIORITY,entryClass));
                    }else if (entryName.contains("Action$$Inject")){
                        clazzs.add(new InjectorPriorityWrapper(InjectorPriorityWrapper.ACTION_PRIORITY,entryClass));
                    }else if(entryName.contains("$$Inject")){
                        ((Injector)entryClass.newInstance()).inject();
                    }
                }
            }

            //進(jìn)行實例化
            if (null != clazzs && clazzs.size() > 0){
                Collections.sort(clazzs);
                for (int i = 0; i < clazzs.size(); i ++){
                    InjectorPriorityWrapper clazz = clazzs.get(i);
                    ((Injector)clazz.mClass.newInstance()).inject();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return clazzs;
    }
}

這個類是得到dex文件下的信息今魔,接著根據(jù)類名稱反射生成這個類(這個類就是我們生成的源碼)勺像,然后進(jìn)行實例化調(diào)用。到這里我們編譯期注解的例子已經(jīng)講解完畢了错森,其實步驟非常固定吟宦,只要操作一遍就熟悉了。如果文章寫得有不懂的你可以下載源碼下來看看涩维,希望大家能在以后的項目中用到這個技術(shù)殃姓,還是非常方便。

總結(jié):本文通過一個實際的例子來說明這項技術(shù)怎么使用激挪,主要步驟包括:項目結(jié)構(gòu)劃分,注解模塊實現(xiàn)辰狡,注解處理器模塊實現(xiàn),注解生成源碼垄分,生成的源碼的使用等宛篇,希望大家能在學(xué)習(xí)本文完有個大的提高,因為你有能力去閱讀這類型框架的源碼了薄湿,而且能自己實現(xiàn)一套復(fù)雜的框架叫倍,希望本篇是你的第一步引導(dǎo)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末豺瘤,一起剝皮案震驚了整個濱河市吆倦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坐求,老刑警劉巖蚕泽,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異桥嗤,居然都是意外死亡须妻,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進(jìn)店門泛领,熙熙樓的掌柜王于貴愁眉苦臉地迎上來荒吏,“玉大人,你說我怎么就攤上這事渊鞋〈赂” “怎么了?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵锡宋,是天一觀的道長儡湾。 經(jīng)常有香客問我,道長执俩,這世上最難降的妖魔是什么徐钠? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮奠滑,結(jié)果婚禮上丹皱,老公的妹妹穿的比我還像新娘。我一直安慰自己宋税,他們只是感情好摊崭,可當(dāng)我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杰赛,像睡著了一般呢簸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上乏屯,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天根时,我揣著相機(jī)與錄音,去河邊找鬼辰晕。 笑死蛤迎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的含友。 我是一名探鬼主播替裆,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼窘问!你這毒婦竟也來了辆童?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤惠赫,失蹤者是張志新(化名)和其女友劉穎把鉴,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體儿咱,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡庭砍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了概疆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逗威。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖岔冀,靈堂內(nèi)的尸體忽然破棺而出凯旭,到底是詐尸還是另有隱情,我是刑警寧澤使套,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布罐呼,位于F島的核電站,受9級特大地震影響侦高,放射性物質(zhì)發(fā)生泄漏嫉柴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一奉呛、第九天 我趴在偏房一處隱蔽的房頂上張望计螺。 院中可真熱鬧夯尽,春花似錦、人聲如沸登馒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陈轿。三九已至圈纺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間麦射,已是汗流浹背蛾娶。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留潜秋,地道東北人蛔琅。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像半等,于是被迫代替她去往敵國和親揍愁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容