Android 編譯時注解解析 —— 自己動手實現(xiàn)一個 Router

前言

類似于 ButterKnife 刁岸、 Dagger 脏里、 ARouter,它們可以通過幾個注解虹曙,就可以實現(xiàn)以往需要很大功夫?qū)懙拇a迫横,還可以非常好的解除依賴鸦难,來提高解耦度,提升代碼的擴(kuò)展空間员淫。
其實它們也是通過一些注解在編譯時生成一部分代碼合蔽,來提高開發(fā)效率或者實現(xiàn)一些特別的功能。

這幾天在準(zhǔn)備春招的過程中對這一塊的內(nèi)容產(chǎn)生了較大的興趣介返,于是也自己動手寫了一個超級簡易版的 Router拴事,功能暫且就只實現(xiàn)跳轉(zhuǎn) Activity。

代碼demo

需要知道的是

編譯時注解

需要對 @Retention 有一個了解圣蝎。

元注解 @Retention

類型 作用
SOURCE 只在 java 源文件中存在刃宵,編譯器編譯后就被丟棄
CLASS 在 Java 源文件和編譯后生成的字節(jié)碼中存在,在虛擬機(jī)運(yùn)行時被丟棄
RUNTIME 運(yùn)行時也存在徘公,可以通過反射讀取注解

這里的 CLASS 就是編譯時注解牲证,我們可以通過這樣的注解在編譯時獲取到自己想要的一些信息,并依據(jù)它們做一些操作关面,來達(dá)到自己的目的坦袍。

注解其它相關(guān)這里就不提了。

APT

APT(annotation processing tool) 是一種處理編譯時注解等太,在編譯階段解析注解并且生成 java 代碼的技術(shù)捂齐,以此來減少開發(fā)時需要寫的代碼,我們這里用的就是 APT 技術(shù)缩抡。

AbstractProcessor

AbstractProcessor 是一個抽象類奠宜,是 Java 中專門用來處理注解的類。

AbstractProcessor 中有兩個需要重寫的方法

    //初始化方法瞻想,提供處理時的環(huán)境
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }
    
    //處理注解過程
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

SPI

如果對虛擬機(jī)的有了解朋友可能會對它有印象压真,因為可以追溯到第二次雙親委派模型破壞。

它是一種動態(tài)替換機(jī)制蘑险,可以動態(tài)加載一些繼承某個接口的實體類滴肿。

AutoService

Android build 不會生成 META-INF,而 SPI 需要根據(jù)聲明在 META-INF 中的類名來執(zhí)行類文件漠其。

這里可以使用 Google 的 AutoService 來創(chuàng)建 META-INF嘴高,并將被注解的文件聲明到 META-INF 中竿音。

JavaPoet

大名鼎鼎的 square 公司的 JavaPoet, 可以有效的幫忙生成代碼和屎。

開始動手

自定義注解

創(chuàng)建一個寫自定義注解的library,注意春瞬,一定要選擇 Java Library

image.png

創(chuàng)建一個自定義注解

//指定被標(biāo)注類型為類或者接口
@Target(ElementType.TYPE)
//指定為編譯時注解
@Retention(RetentionPolicy.CLASS)
public @interface RouterUrl {
    //用來做為跳轉(zhuǎn)路徑
    String url();
}

使用注解

先在 app 的 gradle 中引入此包

    implementation project(':lib_annotation')

接下來為 Activity 標(biāo)注柴信,因為需要跳轉(zhuǎn),這里我們寫兩個 Activty宽气。

//當(dāng)前類
@RouterUrl(url = "main")
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

//跳轉(zhuǎn)目標(biāo)
@RouterUrl(url = "second")
public class SecondActivity extends AppCompatActivity {
    private TextView textView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.tv_text);
        textView.setText("跳轉(zhuǎn)成功~");
    }
}

自定義注解非常簡答随常,接下來看 APT 技術(shù)的核心潜沦,如何在編譯時解析一個注解。

解析注解

創(chuàng)建一個用來解析注解的 library绪氛,同樣也需要 Java 的 library唆鸡,不然將無法使用 AbstractProcessor。

image.png

在它的 gradle 中引用注解包和需要的第三方庫

    implementation 'com.squareup:javapoet:1.8.0'
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation project(':lib_annotation')

創(chuàng)建一個 RouterCompiler 類

//通過 AutoService 將 Processor 聲明到 META-INF 中
@AutoService(Processor.class)
//指定解析的注解
@SupportedAnnotationTypes({"com.example.jzycc.lib_annotation.RouterUrl"})
public class RouterCompiler extends AbstractProcessor {
    private Filer filer;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //獲取 RouterUrl 的集合
        Set< ? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(RouterUrl.class);
        
        //這里是 JavaPoet 的使用枣察,作用是構(gòu)建一個類争占,類名為 WhatRouterTable
        TypeSpec.Builder typeSpec = TypeSpec.classBuilder("WhatRouterTable")
                .addModifiers(Modifier.PUBLIC);

        for (Element element : elements){
            //獲取注解中的 url 信息
            String url = element.getAnnotation(RouterUrl.class).url();
            //將其專成存有類信息的 TypeElement
            TypeElement typeElement = (TypeElement)element;
            //獲取目標(biāo)類的全名
            String className = typeElement.getQualifiedName().toString();
            
            //構(gòu)造一個字段,以 url 為名序目,類名為值臂痕,如 public final static url = "classname";
            FieldSpec fieldSpec = FieldSpec.builder(String.class,url)
                    .addModifiers(Modifier.PUBLIC,Modifier.FINAL,Modifier.STATIC)
                    .initializer("$S",className)
                    .build();
            //將字段添加到 typeSpec 中
            typeSpec.addField(fieldSpec);
        }
        try{
            //指定生成 Java 文件的 包名 (這個包現(xiàn)在還沒建立,接下來它做為提供給開發(fā)者使用的 api)
            String packageFullName = "com.example.jzycc.lib_whatrouter";
            //構(gòu)造 Java 文件
            JavaFile javaFile = JavaFile.builder(packageFullName, typeSpec.build()).build();
            //寫入到 filer猿涨,生成對應(yīng)的 class 文件
            javaFile.writeTo(filer);

        }catch (IOException e){
            e.printStackTrace();
        }

        return true;
    }
}

以上就是處理注解的邏輯握童,我們通過解析注解獲取它的url值以及被此注解表明的類名,并將這些值做為字段添加到我們解析時生成的 Java 文件中叛赚。

如何調(diào)試 APT

直接調(diào)試是無法調(diào)試 APT 代碼的澡绩,首先找到項目中的 gradle.properties 文件,也可以在 .gradle 文件中全局配置俺附。

加入如下代碼

org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

如圖位置我們選擇 Edit Configurations

image.png

點(diǎn)擊下面圖中的 + 號英古,并選擇 Remote

image.png

隨便起個名字,點(diǎn)擊調(diào)試按鈕昙读,如果調(diào)試成功則會提示如下信息

image.png
Connected to the target VM, address: 'localhost:5005', transport: 'socket'

為 APT 代碼加入斷點(diǎn)召调,我們這時候 rebuild 程序就可以開始開始調(diào)試了。

接下來去看下如何使用解析注解得到的信息蛮浑。

Router 的 API

同樣唠叛,我們?yōu)樗⒁粋€ module ,但是這次我們建立的是 Android Library沮稚,因為我們在這里需要使用 Android 的一些類艺沼。

image.png

在這里,我們先運(yùn)行一下代碼蕴掏,看一下注解是否會生成我們想要的 Java 文件障般。

按兩下 shift,搜索我們創(chuàng)建的類名 'WhatRouterTable'

可以發(fā)現(xiàn)我們很成功的生成了我們想要的信息盛杰,最重要的一步已經(jīng)完成了挽荡,接下來我們來完成如何利用這個文件。

package com.example.jzycc.lib_whatrouter;

import java.lang.String;

public class WhatRouterTable {
  public static final String main = "com.example.jzycc.whatrouter.MainActivity";

  public static final String second = "com.example.jzycc.whatrouter.SecondActivity";
}

創(chuàng)建 WhatRouter即供,做為 Api 的提供類定拟。

package com.example.jzycc.lib_whatrouter;

import android.app.Activity;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import java.lang.reflect.Method;

public class WhatRouter {

    private Class clazz;
    private Context context;
    private Class goalClazz;

    private static WhatRouter instance;
    //這里用單例模式來創(chuàng)建此類
    public static WhatRouter getInstance(){
        if (instance == null){
            synchronized (WhatRouter.class){
                if (instance == null){
                    instance = new WhatRouter();
                }
            }
        }

        return instance;
    }

    private WhatRouter(){
        try{
            clazz = Class.forName("com.example.jzycc.lib_whatrouter.WhatRouterTable");
        }catch (Exception e){
            Log.e("WhatRouterError", "WhatRouter: ",e );
        }
    }
    //傳入 context
    public WhatRouter with(Context context){
        this.context = context;
        return this;
    }
    
    //傳入 url
    public WhatRouter url(String url){
        try{
            goalClazz = Class.forName(clazz.getField(url).get("").toString());

        }catch (Exception e){
            Log.e("WhatRouterError", "url: ", e);
        }
        return this;
    }
    
    //啟動 Activity
    public void startActiviity(){
        try{
            Intent intent = new Intent(context, goalClazz);
            context.startActivity(intent);
        }catch (Exception e){
            Log.e("WhatRouterError", "startActiviity: ",e );
        }
    }
}

非常的簡單,我們通過反射去獲取我們在解析注解時生成的類逗嫡,再根據(jù)類中的url字段獲取目標(biāo)類的類名青自,依據(jù)此類名獲取目標(biāo)類用來啟動 Activity株依。

使用一下看看是否成功

修改 MainActivity

@RouterUrl(url = "main")
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //這里我們啟動 SecondActivity
        WhatRouter.getInstance().with(this).url("second").startActiviity();
    }
}

運(yùn)行APP

image.png

成功的跳轉(zhuǎn)到了 SecondActivity。

總結(jié)

看完之后發(fā)現(xiàn)實現(xiàn)一個簡易 Router 是非常簡單的了延窜。

當(dāng)然這只是一個學(xué)習(xí) APT 技術(shù)的 demo, 如果要投入到實際場景當(dāng)中去那是完全不行的恋腕,使用技術(shù)并不難,難的是如何更好的滿足業(yè)務(wù)場景逆瑞,以及更加深入到其中去學(xué)習(xí)它的原理吗坚。

學(xué)習(xí) APT 相關(guān)的知識,我這僅僅是一個開始呆万。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末商源,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谋减,更是在濱河造成了極大的恐慌牡彻,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件出爹,死亡現(xiàn)場離奇詭異庄吼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)严就,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進(jìn)店門总寻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人梢为,你說我怎么就攤上這事渐行。” “怎么了铸董?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵祟印,是天一觀的道長。 經(jīng)常有香客問我粟害,道長蕴忆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任悲幅,我火速辦了婚禮套鹅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘汰具。我一直安慰自己卓鹿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布郁副。 她就那樣靜靜地躺著减牺,像睡著了一般豌习。 火紅的嫁衣襯著肌膚如雪存谎。 梳的紋絲不亂的頭發(fā)上拔疚,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機(jī)與錄音既荚,去河邊找鬼稚失。 笑死,一個胖子當(dāng)著我的面吹牛恰聘,可吹牛的內(nèi)容都是我干的句各。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼晴叨,長吁一口氣:“原來是場噩夢啊……” “哼凿宾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起兼蕊,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤初厚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后孙技,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體产禾,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年牵啦,在試婚紗的時候發(fā)現(xiàn)自己被綠了亚情。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡哈雏,死狀恐怖楞件,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情裳瘪,我是刑警寧澤履因,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站盹愚,受9級特大地震影響栅迄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜皆怕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一毅舆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧愈腾,春花似錦憋活、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春辜梳,著一層夾襖步出監(jiān)牢的瞬間粱甫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工作瞄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留茶宵,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓宗挥,卻偏偏與公主長得像乌庶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子契耿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評論 2 350