組件化頁面路由框架實(shí)現(xiàn)原理

本文 Demo 源碼:https://github.com/asmitaliyao/RouterDemo

前言

在 app 實(shí)現(xiàn)了組件化之后斯嚎,由于組件之間存在代碼隔離,不允許相互引用挨厚,所以組件之間不能進(jìn)行直接溝通堡僻。而在整個(gè) app 中,不可避免地要進(jìn)行頁面跳轉(zhuǎn)疫剃,包括 Activity 和 Fragment 跳轉(zhuǎn)钉疫。也就是說,組件間的頁面跳轉(zhuǎn)巢价,是在組件化開發(fā)過程中一個(gè)必須要面對(duì)的問題牲阁。
解決這個(gè)問題的方式有很多,可以想到的方案是壤躲,可以通過隱式跳轉(zhuǎn)來實(shí)現(xiàn)城菊,但是隨著頁面的增多,intent-filter 的過濾條件會(huì)增多碉克,后期維護(hù)就更加麻煩凌唬。同時(shí),也存在安全隱患漏麦,因?yàn)槠渌?app 也可以通過隱式 intent 跳轉(zhuǎn)到我們的 Activity客税,所以需要設(shè)置 exported = false,確保只有自己的 app 能啟動(dòng)組件撕贞。隱式跳轉(zhuǎn)是原生的方案更耻,和廣播一樣,范圍是整個(gè) Android 系統(tǒng)麻掸。也可以直接通過反射來實(shí)現(xiàn)酥夭,但是這樣會(huì)不可避免地增加很多重復(fù)的代碼。
參考計(jì)算機(jī)網(wǎng)絡(luò)中的路由器概念脊奋,將各個(gè)組件看成不同的局域網(wǎng)熬北,通過路由做中轉(zhuǎn)站,這個(gè)中轉(zhuǎn)站可以攔截一些不安全的跳轉(zhuǎn)诚隙,或者設(shè)定一些特定的攔截服務(wù)讶隐。由此,誕生了一系列 Android 中的頁面路由框架久又,比如阿里巴巴開源的 ARouter 框架巫延。

簡(jiǎn)單說一下路由框架的使用效五,以 ARouter 為例(熟悉的可以直接略過):
1、在 module 中添加路由框架的依賴炉峰。(通常該 module 為組件化單獨(dú)的功能組件 module)

implementation ‘com.alibaba:arouter-api:1.4.0'
annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'

2畏妖、在個(gè)模塊 build.gradle 的 defaultConfig 中加入。

javaCompileOptions {
    annotationProcessorOptions {
        arguments = [moduleName :project.getName() ]
    }
}

3疼阔、在 Application 中初始化路由框架戒劫。

if (BuildConfig.isDebug){
    ARouter.openLog();
    ARouter.openDebug();
    //需要在init之前配置才有效
}
ARouter.init(XXXApplication.this);

4、在支持路由的頁面上添加注解婆廊,配置路由 url迅细。

@Route(path = "/app/main")
public class MainActivity extends BaseActivity {
    ...
}

5、在業(yè)務(wù)代碼中執(zhí)行跳轉(zhuǎn)

 ARouter.getInstance().build("/app/main").navigation();

可以看到淘邻,組件化場(chǎng)景下的路由跳轉(zhuǎn)和原生跳轉(zhuǎn)相比存在以下優(yōu)勢(shì):
1茵典、原生顯示跳轉(zhuǎn)是直接的類依賴,耦合嚴(yán)重宾舅,在組件化中统阿,組件之間相互隔離,直接依賴會(huì)破壞組件化贴浙。路由跳轉(zhuǎn)則是通過 URL 索引砂吞,無需依賴。
2崎溃、原生隱式跳轉(zhuǎn)通過 AndroidManifest 集中管理,維護(hù)困難盯质。路由在各自業(yè)務(wù)模塊中使用注解管理袁串,維護(hù)更加獨(dú)立。
3呼巷、原生跳轉(zhuǎn)擴(kuò)展性差囱修。路由跳轉(zhuǎn)可以統(tǒng)一定義頁面 url,配合數(shù)據(jù)上報(bào)王悍,可以統(tǒng)一實(shí)現(xiàn)頁面跳轉(zhuǎn)相關(guān)的數(shù)據(jù)上報(bào)功能破镰。路由攔截,可以擴(kuò)展實(shí)現(xiàn)登錄狀態(tài)檢測(cè)的攔截压储,可以實(shí)現(xiàn)跳轉(zhuǎn)降級(jí)等等功能鲜漩。

框架功能梳理

通過上面對(duì)路由框架的簡(jiǎn)單了解,可以知道路由框架的核心功能:對(duì)于一個(gè)給定的頁面 URL集惋,根據(jù)映射關(guān)系表孕似,來打開特定的頁面的組件。

需要實(shí)現(xiàn)的頁面路由框架刮刑,主要需要包含下面的能力:
1喉祭、使用 URL 標(biāo)記頁面养渴。
頁面路由框架的核心是根據(jù) URL 和頁面的映射關(guān)系去打開頁面,所以首先就需要我們開發(fā)人員去標(biāo)記出來 URL 和頁面之間的對(duì)應(yīng)關(guān)系泛烙,具體怎么標(biāo)記需要由頁面路由框架提供理卑。參考 ARouter 通過注解標(biāo)記頁面。

2蔽氨、收集 URL 和其標(biāo)記的頁面傻工。
在標(biāo)記了頁面之間的對(duì)應(yīng)關(guān)系之后,路由框架一定需要收集這些關(guān)系孵滞,并統(tǒng)一記錄映射關(guān)系表中捆,這樣才能在運(yùn)行時(shí)根據(jù)映射關(guān)系表來打開對(duì)應(yīng)的頁面。

3坊饶、將 URL 和頁面映射關(guān)系匯總并注冊(cè)在內(nèi)存中泄伪。
比如以 Map 形式使保存 URL 和頁面完整類名的映射關(guān)系。如果在非組件化場(chǎng)景中匿级,比如整個(gè)項(xiàng)目的頁面都在一個(gè)模塊下蟋滴,那么可以直接給該映射關(guān)系表固定命名,在該模塊中直接讀取這樣一個(gè)映射關(guān)系表痘绎。如果在組件化場(chǎng)景中津函,由于組件之間沒有相互依賴,所以上面 1孤页、2 兩步標(biāo)記頁面和收集頁面的過程發(fā)生在每個(gè)子工程組件中尔苦,所以每個(gè)子工程組件中都會(huì)生成一個(gè)映射表。而為了確保整個(gè)應(yīng)用在運(yùn)行期間每個(gè) URL 都能找到對(duì)應(yīng)的頁面行施,我們就需要把所有的映射表在運(yùn)行的時(shí)候注冊(cè)到路由框架中允坚。也就是把每個(gè)子工程組件中的映射表統(tǒng)一到路由框架中。而如果采取手動(dòng)注冊(cè)的方式的話蛾号,就需要在項(xiàng)目下 app 子工程中逐個(gè)去注冊(cè)映射表稠项,這種人工的方式比較麻煩而且可能會(huì)有遺漏,從而導(dǎo)致因?yàn)橛成浔頉]有注冊(cè)鲜结,無法通過 URL 打開頁面展运。針對(duì)這個(gè)問題,路由框架應(yīng)該提供自動(dòng)注冊(cè)的機(jī)制精刷。

4拗胜、提供接口完成打開頁面操作。
開發(fā)者根據(jù)業(yè)務(wù)具體場(chǎng)景調(diào)用路由框架的提供的接口傳入具體的 URL 并調(diào)用路由功能贬养,路由框架根據(jù) URL 在映射表中找到對(duì)應(yīng)的頁面挤土,再打開對(duì)應(yīng)的 Activity,甚至是 Fragment误算。

5仰美、其他可選功能迷殿。
自動(dòng)生成文檔。當(dāng)路由框架收集好了映射關(guān)系之后咖杂,我們可以生成一個(gè)頁面的文檔庆寺,因?yàn)榇蜷_頁面的時(shí)候我們必須得找到這個(gè)頁面對(duì)應(yīng)的 URL 去打開對(duì)應(yīng)頁面,而在工程中的頁面可能很多诉字,不可能每次需要打開頁面都去問一下對(duì)應(yīng)的開發(fā)人員該頁面的 URL 是什么懦尝。所以我們需要在路由框架中幫助生成一個(gè)統(tǒng)一的文檔,記錄 URL 和頁面之間的對(duì)應(yīng)關(guān)系壤圃,當(dāng)我們需要打開某個(gè)頁面的時(shí)候陵霉,自己去查閱文檔即可。
頁面跳轉(zhuǎn)攔截器伍绳。打開頁面的過程中踊挠,可能需要在打開某些頁面的過程中,進(jìn)行攔截冲杀,處理對(duì)應(yīng)的邏輯效床。比如在打開某些需要登錄態(tài)的頁面時(shí),統(tǒng)一檢查登錄態(tài)权谁,如果已登錄就跳轉(zhuǎn)到指定頁面剩檀,如果未登錄則攔截打開登錄頁面。
其中旺芽,第 1 步標(biāo)記頁面沪猴、第 2 步收集頁面、第 3 步注冊(cè)映射三個(gè)步驟都需要在編譯期間完成甥绿,這時(shí)候就可以考慮提供一個(gè) gradle 插件將這些步驟封裝在里面字币,對(duì)于路由框架的使用者來說是非常友好的。

頁面路由——標(biāo)記頁面共缕、收集頁面

對(duì)于一個(gè) url,根據(jù)映射關(guān)系表士复,來打開特定的頁面图谷。核心是建設(shè)一個(gè)頁面 url 到真實(shí)頁面類名的映射關(guān)系表。
最無腦的方式是手動(dòng)維護(hù)這樣的關(guān)系表阱洪。創(chuàng)建一個(gè)映射表工具類便贵,里面提供一個(gè) get() 方法,方法返回 Map 對(duì)象冗荸。在方法中承璃,初始化 Map 對(duì)象后,不停地填入 URL 和頁面的完整類名蚌本。如下:

public class RouterMapping {

    public static Map<String, String> get() {
        Map<String, String> mapping = new HashMap<>();
        mapping.put("router://xxx/xxx", "com.example.xxx.xxx");
        // ...
        return mapping;
    }
}

這種手動(dòng)維護(hù)的方式存在很多問題盔粹。其中一個(gè)問題是太過集中化隘梨,所有的開發(fā)人員都需要共同來維護(hù)這樣一個(gè)獨(dú)立的關(guān)系表。另外一個(gè)問題是在開發(fā)過程中舷嗡,我們可能會(huì)需要重構(gòu)代碼轴猎,真實(shí)的類名或者包名是可能會(huì)發(fā)生變化的,而變化后需要更新這個(gè)關(guān)系表进萄,這種情況下存在遺漏的風(fēng)險(xiǎn)捻脖。
所以我們需要的是一種分布式并且更加自動(dòng)化的方式來維護(hù)映射關(guān)系表。分布式指的是中鼠,每一個(gè)開發(fā)人員在標(biāo)記自己開發(fā)的頁面的時(shí)候可婶,只需要在自己的代碼中添加標(biāo)記即可,不應(yīng)該影響別人的代碼援雇。自動(dòng)化指的是在分布式標(biāo)記的前提下矛渴,自動(dòng)匯總成一個(gè)最終的映射關(guān)系表。
這個(gè)時(shí)候就需要引入一項(xiàng)很方便的技術(shù):APT熊杨。

APT

APT 概述

APT 即 Annotation Processing Tool曙旭。它是 javac 的一個(gè)工具,中文意思為編譯時(shí)注解處理器晶府。
注解兜粘,Annotation桂敛,可以理解為一種用來描述數(shù)據(jù)的標(biāo)注。這里被描述的數(shù)據(jù)可以是類:比如 MainActivity,也可以是方法岭洲,也可以是變量。在 Java 中逻翁,類龙填、方法、變量都是可以被注解進(jìn)行標(biāo)注的尸曼。以 @Override 注解為例们何,我們?cè)趧?chuàng)建 Activity 時(shí)經(jīng)常會(huì)看到它。它是用來標(biāo)注重寫父類方法的注解控轿。假如我們?nèi)サ袅?@Override 注解冤竹,仍然是可以編譯通過的。但是如果我們給一個(gè)不是重寫父類的方法添加了 @Override 注解茬射,那么編譯的時(shí)候就會(huì)報(bào)錯(cuò)鹦蠕,使用 IDE 的話也會(huì)在編寫代碼的時(shí)候錯(cuò)誤提示出來。
即使我們?cè)诖a中給方法標(biāo)記了 @Override 注解在抛,但是如果在代碼中沒有一個(gè)角色來對(duì)標(biāo)注的注解進(jìn)行識(shí)別和處理的話钟病,這些標(biāo)記其實(shí)是沒有用的。所以需要有個(gè)角色來識(shí)別和處理我們標(biāo)記的注解。這個(gè)角色就是 APT 即注解處理器肠阱。首先我們知道票唆,java 代碼是用 javac 來編譯的,而確切的說注解處理器是 javac 的一個(gè)工具辖所,它用來在編譯時(shí)掃描和處理注解惰说。在源代碼的編譯階段,我們可以通過 APT 來掃描代碼中的注解相關(guān)的內(nèi)容缘回,獲取到注解和被注解對(duì)象的相關(guān)信息吆视。最常用的用法就是在編譯階段通過掃描注解獲取到相關(guān)信息后來動(dòng)態(tài)地生成一些代碼,通常都是一些具有規(guī)律性的重復(fù)代碼酥宴,省去了手動(dòng)編寫的工作啦吧。獲取注解及生成代碼都是在代碼編譯的時(shí)候完成的,相比反射在運(yùn)行時(shí)處理注解大大提高了程序性能拙寡。APT 的優(yōu)點(diǎn)就是簡(jiǎn)單授滓、方便,可以減少很多重復(fù)的代碼肆糕,這一點(diǎn)從我們 Android 項(xiàng)目中使用的 EventBus 注解框架就可以感受到般堆。

APT 基本開發(fā)流程

1、創(chuàng)建注解工程诚啃,定義注解淮摔。
2、創(chuàng)建注解處理器工程始赎,編寫注解處理器和橙。
3、在業(yè)務(wù)模塊中調(diào)用注解與注解處理器造垛。

下面就是 Demo 中具體的實(shí)現(xiàn)魔招。

標(biāo)記頁面

定義注解:@Destination
1、建立注解工程
建立注解子工程:router-annotations
配置 build.gradle 文件:

// 1五辽、應(yīng)用 java 插件
plugins {
    id 'java-library'
}

// 2办斑、設(shè)置源碼兼容性
java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

配置 settings.gradle

include ':router-annotations'

2、定義注解
在注解子工程中創(chuàng)建注解接口:Destination

@Target({ElementType.TYPE}) // 元注解杆逗,說明當(dāng)前注解可以修飾的元素俄周,此處標(biāo)識(shí)可以用于標(biāo)記在類上面
@Retention(RetentionPolicy.CLASS) // 元注解,說明當(dāng)前注解的生命周期髓迎。也就是可以保留的時(shí)間。保留到編譯為 class 文件建丧。
public @interface Destination {

    /**
     * 當(dāng)前頁面定義的 url排龄,不能為空
     * @return 頁面定義的 url
     */
    String url();

    /**
     * 定義當(dāng)前頁面的描述
     * @return 頁面描述內(nèi)容
     */
    String description() default "no description";
}

3、使用注解
在業(yè)務(wù)代碼中添加注解依賴:

implementation project(':router-annotations')

使用注解:

@Destination(url = "/app/first", description = "first page")
public class FirstActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);
    }
}

收集頁面

實(shí)現(xiàn)注解處理器:DestinationProcessor
1、建立注解處理器工程
建立注解子工程:router-processor
配置 build.gradle 文件:

// 1橄维、應(yīng)用 java 插件
plugins {
    id 'java-library'
}

// 2尺铣、設(shè)置源碼兼容性
java {
    sourceCompatibility = JavaVersion.VERSION_1_7
    targetCompatibility = JavaVersion.VERSION_1_7
}

// 3、添加注解工程的依賴
dependencies {
    implementation project(':router-annotations')
}

2争舞、定義注解處理類
在注解處理器子工程中創(chuàng)建注解處理類 DestinationProcessor凛忿,主要負(fù)責(zé)采集注解信息:

public class DestinationProcessor extends AbstractProcessor {

    private static final String TAG = "DestinationProcessor";

    /**
     * 告訴編譯器當(dāng)前注解處理器支持處理哪些注解
     * 在這里返回之后,Javac 就會(huì)幫我們收集對(duì)應(yīng)的注解竞川,傳給 DestinationProcessor
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(
                Destination.class.getCanonicalName()
        );
    }

    /**
     * 編譯器幫我們收集到我們需要的注解后店溢,會(huì)回調(diào)的方法
     * @param set 編譯器幫我們收集到的注解信息
     * @param roundEnvironment 當(dāng)前的編譯環(huán)境
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 避免多次調(diào)用 process
        if (roundEnvironment.processingOver()) {
            return false;
        }

        print("process called");
        // 獲取所有標(biāo)記了 @Destination 注解的類的信息
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Destination.class);
        print("all Destination elements size = " + elements.size());
        // 當(dāng)未搜集到 @Destination 注解標(biāo)注的類的信息時(shí),跳過
        if (elements.isEmpty()) {
            print("process finish");
            return false;
        }

        parseRoutes(elements);

        print("process finish");

        return false;
    }

    private void parseRoutes(Set<? extends Element> elements) {
        // 遍歷所有 @Destination 注解標(biāo)注的類
        for (Element element : elements) {
            final TypeElement typeElement = (TypeElement) element;
            // 嘗試在當(dāng)前類上獲取 @Destination 的信息
            final Destination destination = typeElement.getAnnotation(Destination.class);
            if (destination == null) {
                continue;
            }
            final String url = destination.url();
            final String description = destination.description();
            final String realClassName = typeElement.getQualifiedName().toString();
            print("url = " + url);
            print("description = " + description);
            print("realClassName = " + realClassName);
        }
    }

    private void print(String text) {
        System.out.println(TAG + " >>>>>> " + text);
    }
}

3委乌、注冊(cè)注解處理器
在 src/main/ 目錄下創(chuàng)建 META-INF 目錄床牧,并在其中創(chuàng)建 service/javax.annotation.process.processor 目錄,javac 編譯器會(huì)順著此目錄和文件名查找遭贸,在文件名對(duì)應(yīng)的文件中戈咳,把 DestinationProcessor 類的全類名標(biāo)注進(jìn)去。
或者更推薦使用 google 的 auto-service 庫壕吹,幫助我們自動(dòng)完成上述步驟著蛙,更加簡(jiǎn)單、便捷耳贬。
router-processor 子工程的 build.gradle 中添加依賴:

implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

然后在 DestinationProcessor 類中添加注解:

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

然后需要在各個(gè)業(yè)務(wù)模塊中添加注解處理器依賴:

annotationProcessor project(':router-processor')

最后可以通過命令 ./gradlew :app:assembleDebug -q 編譯驗(yàn)證:


采集標(biāo)注.png

4踏堡、統(tǒng)一記錄映射關(guān)系表
自動(dòng)生成映射表類:

private void parseRoutes(Set<? extends Element> elements, RoundEnvironment roundEnvironment) {

    print("generate method get()");
    ClassName hashMap = ClassName.get("java.util", "HashMap");
    ClassName map = ClassName.get("java.util", "Map");
    ClassName string = ClassName.get("java.lang", "String");
    ParameterizedTypeName mapOfStringString = ParameterizedTypeName.get(map, string, string);

    MethodSpec.Builder builder = MethodSpec.methodBuilder("get")
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
            .returns(mapOfStringString)
            .addStatement("$T mapping = new $T<>()", mapOfStringString, hashMap);
    for (Element element : elements) {
        final TypeElement typeElement = (TypeElement) element;
        // 嘗試在當(dāng)前類上獲取 @Destination 的信息
        final Destination destination = typeElement.getAnnotation(Destination.class);
        if (destination == null) {
            continue;
        }
        final String url = destination.url();
        final String description = destination.description();
        final String realClassName = typeElement.getQualifiedName().toString();
        print("url = " + url);
        print("description = " + description);
        print("realClassName = " + realClassName);

        builder.addStatement("mapping.put($S, $S)", url, realClassName);
    }
    builder.addStatement("return mapping");
    MethodSpec get = builder.build();

    String className = "RouterMapping_" + System.currentTimeMillis();   // 生成的類的類名
    print("generate class " + className);
    TypeSpec clazzRouterMapping = TypeSpec.classBuilder(className)
            .addModifiers(Modifier.PUBLIC)
            .addMethod(get)
            .build();

    print("generate java file");
    JavaFile javaFile = JavaFile.builder("com.example.router.mapping", clazzRouterMapping)
            .build();

    print("write java file to...");
    try {
        javaFile.writeTo(processingEnv.getFiler());
        print("java file write to filer, success");
    } catch (IOException e) {
        print("java file write to filer, error = " + e);
    }
}

再次執(zhí)行編譯后,可在對(duì)應(yīng)模塊的 build/generated/ap_generated_sources/ 內(nèi)找到對(duì)應(yīng)包名路徑的 java 文件效拭。也可以在打包好的 apk 文件中查看 classes.dex暂吉。
自動(dòng)生成 .java 文件的代碼為第三方的 sdk 提供的相應(yīng)的 api ,具體使用:https://github.com/square/javapoet

頁面路由——匯總映射表

雖然前面我們已經(jīng)通過注解和注解處理器生成好了頁面映射關(guān)系表缎患,但是組件化場(chǎng)景下慕的,整個(gè)應(yīng)用工程是由多個(gè)子工程甚至第三方依賴組成的,這些子工程組件尤其是業(yè)務(wù)組件挤渔,可能會(huì)包含相應(yīng)的 Activity 頁面肮街。所以通過 APT 生成的頁面映射關(guān)系表,在每個(gè)子工程下是各自獨(dú)立生成的判导。這樣的話嫉父,在一個(gè) app 中就可能擁有多份頁面映射表。在應(yīng)用程序運(yùn)行期間眼刃,為了可以實(shí)現(xiàn)跨組件路由頁面绕辖,就必須把所有的這些頁面映射關(guān)系表找到,并且注冊(cè)到內(nèi)存中去擂红。
這種場(chǎng)景下仪际,無論是子工程的代碼還是 aar 包里面的代碼,最終都會(huì)以 .class 字節(jié)碼的形式存在,然后一起被打包成為 dex 文件树碱。所以就可以采用特定的技術(shù)捕捉到這個(gè)時(shí)間點(diǎn)肯适,解析 .class 中的字節(jié)碼信息,找到其中的映射表類成榜,把這些類匯總起來框舔,然后生成一個(gè)具有固定名稱的映射表。在后續(xù)運(yùn)行的時(shí)候赎婚,只需要注冊(cè)這一個(gè)固定名稱的總的映射表就行了刘绣。
在這個(gè)場(chǎng)景中,需要使用到的技術(shù)即:字節(jié)碼插樁惑淳。

字節(jié)碼插樁

字節(jié)碼:

開發(fā)人員平時(shí)編寫的代碼额港,一般是 java 或者 kotlin 文件,這些文件在編譯的時(shí)候其實(shí)都會(huì)被 javac 或者 kotlinc 編譯成為 .class 文件歧焦,這個(gè) .class 文件其實(shí)就是字節(jié)碼文件移斩。字節(jié)碼是 java 虛擬機(jī)執(zhí)行的指令的格式。字節(jié)碼隨后會(huì)被編譯成為 dex 文件绢馍,最終被打包到 apk 里面向瓷,然后在用戶的手機(jī)上運(yùn)行。

字節(jié)碼插樁:

插樁是保證程序在原有的邏輯完整的基礎(chǔ)上舰涌,在程序中插入一些代碼段猖任,從而達(dá)到一些諸如信息采集的目的。通俗來說瓷耙,插樁就是把一段代碼通過某種策略插入到另一段代碼中去朱躺,或者是替換掉另一段代碼。而字節(jié)碼插樁就是在 .class 文件轉(zhuǎn)化為 dex 文件之前修改 .class 文件搁痛,從而達(dá)到修改或替換代碼的目的长搀。

應(yīng)用場(chǎng)景:

代碼插入:比如如果需要監(jiān)控應(yīng)用程序里面方法的所有執(zhí)行耗時(shí)。面對(duì)這種大量的重復(fù)性的問題鸡典,首先需要考慮自動(dòng)化解決源请。通過字節(jié)碼插樁掃描每個(gè)編譯好的 class 文件,并且使用特定規(guī)則彻况,修改字節(jié)碼谁尸,達(dá)到監(jiān)控方法耗時(shí)的目的。
代碼替換:比如如果需要將項(xiàng)目中用到的某種的方法纽甘,例如 dialog.show() 方法良蛮,替換為我們自己包裝過的方法。全局快捷鍵替換悍赢,有錯(cuò)誤替換風(fēng)險(xiǎn)背镇,同時(shí)如果一些第三方的方法也用到了這個(gè)方法咬展,這個(gè)時(shí)候全局快捷鍵替換就替換不了了。這個(gè)時(shí)候就可以在 class 編譯成 dex 之前瞒斩,掃描每個(gè) class 文件,并把對(duì)應(yīng)方法的調(diào)用涮总,統(tǒng)一替換胸囱。這種替換方式既可以避免出錯(cuò),又可以修改到第三方 jar 包中的方法瀑梗。
無痕埋點(diǎn)烹笔、性能監(jiān)控等等場(chǎng)景,很多都用到了字節(jié)碼插樁技術(shù)抛丽。很多框架其實(shí)也是在編譯期生成了代碼谤职,從而省去了開發(fā)人員的操作。

技術(shù)原理:

.java -> .class -> .dex -> .apk
1亿鲜、怎么捕捉到 .class 轉(zhuǎn)換成為 .dex 的時(shí)間點(diǎn)允蜈?
Android 提供了 Transform 接口:A.class -> ASM -> A'.class。只需要實(shí)現(xiàn)一個(gè) gradle 插件蒿柳,在插件中提供一個(gè)自定義的 Transform饶套,然后將其注冊(cè)到構(gòu)建過程中,就可以在 .class 轉(zhuǎn)化為 .dex 之前收到相應(yīng)的回調(diào)垒探。在這個(gè)方法的回調(diào)里面妓蛮,我們將會(huì)拿到已經(jīng)編譯好的全部的 .class 的集合。然后我們需要把目標(biāo) .class 文件進(jìn)行修改圾叼,得到我們最終的 .class 文件蛤克。

2、如何對(duì) .class 文件進(jìn)行修改和解析夷蚊?
.class 文件是一種具有特定格式的二進(jìn)制文件构挤,如果手動(dòng)去解析的話其實(shí)是比較麻煩的,我們可以借助一個(gè)名為 ASM 的工具撬码,可以比較方便地去解析儿倒、修改甚至是生成 .class 文件。這樣我們可以稍微忽略掉 .class 文件內(nèi)部的復(fù)雜結(jié)構(gòu)呜笑,專注在字節(jié)碼插樁這個(gè)事情本身上了夫否。

3、什么是 ASM叫胁?
ASM 是一個(gè)字節(jié)碼操作庫凰慈,它可以直接修改已經(jīng)存在的 class 文件或者生成 class 文件。 ASM 提供了一系列便捷的功能來操作字節(jié)碼內(nèi)容驼鹅,與其它字節(jié)碼的操作框架相比(例如 AspectJ)微谓,ASM 更加偏向于底層森篷,直接操作字節(jié)碼,在設(shè)計(jì)上更小豺型、更快仲智,性能上更好,而且?guī)缀蹩梢孕薷娜我庾止?jié)碼姻氨。

Gradle 插件

基本概念

Gradle 是一個(gè)構(gòu)建工具钓辆,負(fù)責(zé)讓工程構(gòu)建變得更加自動(dòng)化。不過肴焊,gradle 只是一個(gè)執(zhí)行環(huán)境前联,提供了基本的框架,而真正的構(gòu)建行為并不是由它來提供娶眷。gradle 負(fù)責(zé)在運(yùn)行的時(shí)候找到所有需要執(zhí)行的任務(wù)似嗤,依次執(zhí)行。真正的任務(wù)届宠,可以由我們手動(dòng)創(chuàng)建任務(wù)提供烁落,比如可以在自定義任務(wù)里面去編譯工程的 java 代碼。但是幾乎所有 android 團(tuán)隊(duì)都需要去編譯 java 代碼席揽,而如果讓所有團(tuán)隊(duì)自己去實(shí)現(xiàn)編譯 java 代碼的任務(wù)的話顽馋,是極不合理的,這個(gè)時(shí)候就需要插件幌羞。
在 gradle 的世界中寸谜,幾乎所有的功能都是以插件的方式提供的。插件負(fù)責(zé)封裝 gradle 運(yùn)行期間需要的 task属桦,在工程中依賴某個(gè)插件之后熊痴,就能復(fù)用這個(gè)插件提供的構(gòu)建行為,增強(qiáng)了 gradle 代碼的可讀性聂宾。gradle 內(nèi)置了很多核心的語言插件果善,基本上能夠滿足大部分的構(gòu)建工作,但是有的插件沒有內(nèi)置系谐,或者有些功能沒有提供巾陕,這個(gè)時(shí)候就可以通過自定義插件來解決。比如 Android Gradle 插件就是基于 Java 插件來拓展的纪他,它在編譯 Java 代碼的基礎(chǔ)上鄙煤,還提供了編譯資源、打包 Apk 的功能茶袒。
總的來說梯刚,gradle 插件負(fù)責(zé)提供具體的構(gòu)建功能(Task),提高了代碼的復(fù)用性薪寓。

如何使用 Gradle 插件

Gradle 插件主要有兩種類型亡资,二進(jìn)制插件和腳本插件澜共。
1、二進(jìn)制插件
通常是實(shí)現(xiàn)了 plugin 接口锥腻,它可以存在于一個(gè)獨(dú)立的編譯腳本里面嗦董,也可以作為一個(gè)獨(dú)立的工程去維護(hù)。這些插件最終會(huì)對(duì)外發(fā)布成一個(gè)插件 jar 包旷太。我們平時(shí)使用得最多的二進(jìn)制插件其實(shí)就是 android 插件展懈。
使用二進(jìn)制插件通常需要三大步驟:
1)聲明插件 id 和版本號(hào)
在項(xiàng)目根目錄的 build.gradle 里,找到 buildscript 代碼塊中的 dependencies 代碼塊供璧,這里的聲明負(fù)責(zé)告訴 gradle 去哪里找對(duì)應(yīng)的插件,也就是使用插件的名稱和版本號(hào)冻记,例如 android 插件:

buildscript {
    dependencies {
        classpath 'com.android.tools.build:gradle:4.0.2'
    }
}

創(chuàng)建 Android 工程時(shí)睡毒,Android Studio 會(huì)默認(rèn)添加好這些信息,不過如果后續(xù)需要升級(jí)插件版本冗栗,則需要修改這里的版本號(hào)演顾。聲明好這些之后,gradle 會(huì)將插件下載到本地隅居,但是還未實(shí)際將插件和工程進(jìn)行綁定钠至。

2)應(yīng)用插件
在 app 子工程的 build.gradle 文件中通過 apply 關(guān)鍵字使用插件,例如:

apply plugin: 'com.android.application'

3)插件參數(shù)配置
在 apply 插件后胎源,我們可能還需要對(duì)插件進(jìn)行一些參數(shù)上的配置棉钧,是否需要配置是由插件自己去定義的。比如對(duì)于一些 android 應(yīng)用來說涕蚤,我們還需要指定它的 sdk 版本宪卿、包名等信息,例如:

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    ...
}

2万栅、腳本插件
它相比二進(jìn)制插件顯得更加輕量級(jí)一些佑钾,因?yàn)樗且粋€(gè)獨(dú)立的 gradle 腳本,腳本中通撤沉#可以對(duì)工程的 build.gradle 腳本進(jìn)行進(jìn)一步的配置或補(bǔ)充休溶。這個(gè)腳本它既可以存在于工程的目錄里面,也可以存在于某個(gè)遠(yuǎn)程服務(wù)器地址中扰她。一般來說兽掰,插件最開始的形式回事一個(gè)腳本插件,因?yàn)橹恍枰陆ㄒ粋€(gè)腳本即可開始開發(fā)义黎,等到腳本中的代碼需要復(fù)用之后禾进,會(huì)需要考慮把腳本插件包裝成二進(jìn)制插件,方便在不同的團(tuán)隊(duì)或者工程里面共享廉涕。
腳本插件之所以輕量泻云,是因?yàn)樗皇枪こ讨械囊粋€(gè)獨(dú)立腳本艇拍,所以腳本插件的使用方法也很簡(jiǎn)單:
1)創(chuàng)建腳本文件,并編寫腳本代碼宠纯。
示例:工程根目錄下創(chuàng)建腳本文件 test.gradle卸夕,腳本中添加打印信息。

2)在需要使用的子工程 build.gradle 文件中聲明即可婆瓜。格式為: apply from: 腳本路徑快集。
示例:apply from: project.rootProject.file("test.gradle")

如何開發(fā) Gradle 插件

gradle 內(nèi)置的各種核心語言插件可以滿足大部分的構(gòu)建工作,但有些插件沒有內(nèi)置或有些功能沒有提供廉白,這個(gè)時(shí)候就可以通過自定義插件來解決个初。
這里主要介紹二進(jìn)制插件的開發(fā)方式,主要包括三大步:
1)建立插件工程猴蹂,在插件工程里面配置好插件的入口院溺。
2)實(shí)現(xiàn)插件內(nèi)部邏輯,以及可能會(huì)需要編寫插件的參數(shù)注入邏輯磅轻。
3)發(fā)布與使用插件珍逸。

下面就是 Demo 中的具體的實(shí)現(xiàn)。

建立 Transform

建立 Transform 類聋溜,并且注冊(cè)到 gradle plugin 里面谆膳。

1、建立 buildSrc 子工程
首先在項(xiàng)目根目錄下創(chuàng)建文件夾且命名為 buildSrc(命名必須為 buildSrc )撮躁,然后在 buildSrc 目錄下創(chuàng)建文件且命名為 build.gradle漱病,在其中按順序編寫以下代碼:

// 1、引入 groovy 插件馒胆,編譯插件工程中的代碼
apply plugin: 'groovy'

// 2缨称、聲明倉庫的地址
repositories {
    mavenCentral()
    google()
}

// 3、聲明依賴的包
dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:4.2.1'
}

2祝迂、編寫 RouterMappingTransform.groovy 類
在 buildSrc 目錄下建立一個(gè)源碼目錄 src睦尽,接著在 src 下建立 main 目錄,再在 main 下建立 groovy 子目錄型雳。
在添加類之前当凡,需要建立好包結(jié)構(gòu),所以在 groovy 目錄下纠俭,建立 com/example/router/gradle 目錄路徑沿量,所以包名將會(huì)是 com.example.router.gradle,然后在 gradle 包下新建 groovy 文件 RouterMappingTransform.groovy冤荆。在其中添加以下代碼:

class RouterMappingTransform extends Transform {

    /**
     * 返回當(dāng)前 Transform 名稱朴则,這個(gè)名稱會(huì)被打印到 gradle 的日志里面
     * @return
     */
    @Override
    String getName() {
        return "RouterMappingTransform"
    }

    /**
     * 返回對(duì)象的作用是用來告知編譯器,當(dāng)前 Transform 需要消費(fèi)的輸入類型钓简。
     * 也就是我們需要編譯器幫我們傳入的對(duì)象的類型乌妒。
     * 這里我們要處理的對(duì)象是 class汹想,所以要求編譯器安徽 class 類型。
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 用來告訴編譯器撤蚊,當(dāng)前的 Transform 需要作用的范圍是在哪里古掏。
     * 是整個(gè)工程還是當(dāng)前子工程。
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    /**
     * 告訴編譯器單簽 Transform 是否支持增量
     * 通常直接返回 false
     * @return
     */
    @Override
    boolean isIncremental() {
        return false
    }

    /**
     * 當(dāng)編譯器把所有的 class 都收集好以后侦啸,會(huì)將它們打包成為 TransformInvocation
     * 然后通過這個(gè)方法將打包好的結(jié)果回調(diào)給我們
     * 所以我們就可以在這個(gè)方法里面對(duì)回調(diào)給我們的 class 作二次處理槽唾。
     * @param transformInvocation
     * @throws TransformException
     * @throws InterruptedException
     * @throws IOException
     */
    @Override
    void transform(TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        // 1、遍歷所有的 input
        // 2光涂、對(duì) input 進(jìn)行二次處理
        // 3庞萍、將 input 拷貝到目標(biāo)目錄
        // 其中 1、3 步是固定的
        
        // 遍歷所有的 input
        transformInvocation.inputs.each {
            // 把工程中文件夾類型的輸入拷貝到目標(biāo)目錄
            it.directoryInputs.each {directoryInput ->
                def destDir = transformInvocation.outputProvider
                        .getContentLocation(
                                directoryInput.name,
                                directoryInput.contentTypes,
                                directoryInput.scopes,
                                Format.DIRECTORY)

                FileUtils.copyDirectory(directoryInput.file, destDir)
            }
            // 把工程中 jar 類型的輸入拷貝到目標(biāo)目錄
            it.jarInputs.each {jarInput ->
                def dest = transformInvocation.outputProvider
                        .getContentLocation(
                                jarInput.name,
                                jarInput.contentTypes,
                                jarInput.scopes,
                                Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}

3忘闻、注冊(cè) RouterMappingTransform
然后在 gradle 包下新建 groovy 文件 RouterPlugin.groovy挂绰。在其中添加以下代碼:

class RouterPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        // 當(dāng)采用 apply 關(guān)鍵字在工程里面去引用插件的時(shí)候,apply 方法里面的邏輯將會(huì)被執(zhí)行
        // 所以這里可以寫注入插件的邏輯服赎,比如往工程里面動(dòng)態(tài)添加 task
        println("RouterPlugin, apply from $project.name")

        // 判斷當(dāng)前工程是否有 com.android.application
        if (project.plugins.hasPlugin(AppPlugin)) {
            // 注冊(cè) Transform
            AppExtension appExtension = project.extensions.getByType(AppExtension)
            Transform transform = new RouterMappingTransform()
            appExtension.registerTransform(transform)
        }
    }
}

在 buildSrc 的 main 目錄下新建 resources 目錄,并在其中建立子目錄 META-INF交播,再在其中添加子目錄 gradle-plugins重虑,在 gradle-plugin 目錄下新建 com.example.router.properties 文件。在其中添加以下代碼:

implementation-class=com.example.router.gradle.RouterPlugin

在 app 子工程下的 build.gradle 文件中添加以下代碼后秦士,執(zhí)行編譯命令缺厉,即可看到輸出內(nèi)容 RouterPlugin, apply from app。

plugins {
    id 'com.android.application'
    id 'com.example.router'  // 添加的代碼隧土,引入 gradle 插件
}

在 app 工程下 build/intermediates/transforms/ 目錄下能夠看到生成的 RouterMappingTransform 文件夾提针,即代表 transform 操作成功。

收集目標(biāo)類

transform 操作成功后曹傀,下面就要開始收集 RouterMapping_xxx.class 文件了辐脖。
在 gradle 包下新建 groovy 文件 RouterMappingCollector.groovy ,并編寫以下代碼:

class RouterMappingCollector {

    private static final String PACKAGE_NAME = "com/example/router/mapping"
    private static final String CLASS_NAME_PREFIX = "RouterMapping_"
    private static final String CLASS_FILE_SUFFIX = ".class"

    private final Set<String> mappingClassNames = new HashSet<>()

    /**
     * 獲取收集到的映射表類名
     * @return
     */
    Set<String> getMappingClassNames() {
        return mappingClassNames
    }

    /**
     * 收集傳遞進(jìn)來的 class 文件或者 class 文件目錄中的映射表類
     * @param classFile
     */
    void collect(File classFile) {
        if (classFile == null || !classFile.exists()) return
        if (classFile.isFile()) {
            // 是 class 文件
            if (classFile.absolutePath.contains(PACKAGE_NAME)
                    && classFile.name.startsWith(CLASS_NAME_PREFIX)
                    && classFile.name.endsWith(CLASS_FILE_SUFFIX)) {
                // 同時(shí)滿足:1皆愉、絕對(duì)路徑包含包名嗜价。2、文件名為"RouterMapping_"開頭幕庐。3久锥、文件名以".class"結(jié)尾。
                String className = classFile.name.replace(CLASS_FILE_SUFFIX, "")
                mappingClassNames.add(className)
            }
        } else {
            // 是一個(gè)目錄
            classFile.listFiles().each {
                collect(it)
            }
        }
    }

    /**
     * 收集 jar 包中的映射表類
     * @param jarFile
     */
    void collectFromJarFile(File jarFile) {
        Enumeration enumeration = new JarFile(jarFile).entries()

        while (enumeration.hasMoreElements()) {
            JarEntry jarEntry = enumeration.nextElement()
            String entryName = jarEntry.name

            if (entryName.contains(PACKAGE_NAME)
                    && entryName.contains(CLASS_NAME_PREFIX)
                    && entryName.contains(CLASS_FILE_SUFFIX)) {
                String className = entryName
                        .replace(PACKAGE_NAME, "")
                        .replace("/", "")
                        .replace(CLASS_FILE_SUFFIX, "")
                mappingClassNames.add(className)
            }
        }
    }
}

clean 以后重新編譯工程异剥,可以看到下面的日志:


收集目標(biāo)類.png

生成匯總映射表

1瑟由、首先規(guī)劃一下最終生成好匯總映射表類的內(nèi)容,類似下面的代碼:

public class RouterMapping {
    
    public static Map<String, String> get() {
        Map<String, String> mapping = new HashMap<>();
        
        mapping.putAll(RouterMapping_1.get());
        mapping.putAll(RouterMapping_2.get());
        // ...
        
        return mapping;
    }
}

2冤寿、開始編碼實(shí)現(xiàn)生成匯總的映射表歹苦。
在 gradle 包下新建 groovy 文件 RouterMappingByteCodeBuilder.groovy 青伤,并編寫以下代碼:

class RouterMappingByteCodeBuilder implements Opcodes{

    public static final String CLASS_NAME = "com/example/router/mapping/RouterMapping"

    static byte[] get(Set<String> allMappingNames) {
        // 1、創(chuàng)建一個(gè)類
        // 2暂氯、創(chuàng)建一個(gè)構(gòu)造方法(手動(dòng)生成字節(jié)碼的時(shí)候潮模,構(gòu)造方法需要由我們手動(dòng)創(chuàng)建)
        // 3、創(chuàng)建一個(gè) get() 方法
        //  1)創(chuàng)建一個(gè) map
        //  2)向 map 中裝入所有映射表的內(nèi)容
        //  3)返回 map
    }

}

其中痴施,我們需要在 get 方法中實(shí)現(xiàn) 1擎厢、2、3 步邏輯對(duì)應(yīng)的字節(jié)碼辣吃,并返回 byte[]动遭。直接編寫 java 字節(jié)碼,其實(shí)門檻是比較高的神得,因?yàn)槲覀儾粌H需要去關(guān)注具體的邏輯的實(shí)現(xiàn)厘惦,還必須確保我們生成的字節(jié)碼是符合虛擬機(jī)規(guī)范的。這里我們引入一個(gè) ASM 工具哩簿,它把字節(jié)碼相關(guān)的操作都封裝成了一系列接口供我們調(diào)用(但是這個(gè) ASM 工具提供的接口其實(shí)也很多很復(fù)雜)宵蕉。

Android Studio -> Preferences -> 搜索 plugin -> 搜索 ASM Bytecode Viewer Sypport Kotlin,安裝并重啟节榜。然后再 RouterMapping.java 類上右鍵選擇 ASM Bytecode Viewer羡玛,幫助查看對(duì)應(yīng)的字節(jié)碼文件。
如下圖:


查看字節(jié)碼文件.png

選擇 ASMified Tab 選項(xiàng)卡宗苍,可以看到工具幫助我們生成的編寫字節(jié)碼的 java 代碼稼稿。


編寫字節(jié)碼的 java 代碼.png

下面就可以開始參考工具生成的代碼開始編寫 RouterMappingBytecodeBuilder 的代碼:

class RouterMappingBytecodeBuilder implements Opcodes {

    public static final String CLASS_NAME = "com/example/router/mapping/RouterMapping"

    static byte[] get(Set<String> allMappingNames) {
        // 1、創(chuàng)建一個(gè)類
        // 2讳窟、創(chuàng)建一個(gè)構(gòu)造方法(手動(dòng)生成字節(jié)碼的時(shí)候让歼,構(gòu)造方法需要由我們手動(dòng)創(chuàng)建)
        // 3、創(chuàng)建一個(gè) get() 方法
        //  1)創(chuàng)建一個(gè) map
        //  2)向 map 中裝入所有映射表的內(nèi)容
        //  3)返回 map

        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
        MethodVisitor methodVisitor

        // 創(chuàng)建類
        classWriter.visit(V1_8,
                ACC_PUBLIC | ACC_SUPER,
                CLASS_NAME,
                null,
                "java/lang/Object",
                null)

        classWriter.visitSource("RouterMapping.java", null);

        // 創(chuàng)建構(gòu)造方法
        methodVisitor = classWriter.visitMethod(
                ACC_PUBLIC,
                "<init>",
                "()V",
                null,
                null)
        methodVisitor.visitCode()   // 開啟字節(jié)碼的生成或訪問丽啡,下面開始寫字節(jié)碼指令

        methodVisitor.visitVarInsn(ALOAD, 0)
        methodVisitor.visitMethodInsn(INVOKESPECIAL,
                "java/lang/Object",
                "<init>",
                "()V",
                false)
        methodVisitor.visitInsn(RETURN)

        methodVisitor.visitMaxs(1, 1)
        methodVisitor.visitEnd()    // 關(guān)閉字節(jié)碼的生成或訪問

        // 創(chuàng)建 get() 方法
        methodVisitor = classWriter.visitMethod(
                ACC_PUBLIC | ACC_STATIC,
                "get",
                "()Ljava/util/Map;",
                "()Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;",
                null)
        methodVisitor.visitCode()

        methodVisitor.visitTypeInsn(NEW, "java/util/HashMap")   // 創(chuàng)建一個(gè) map
        methodVisitor.visitInsn(DUP)    // 將其入棧
        methodVisitor.visitMethodInsn(INVOKESPECIAL,
                "java/util/HashMap",
                "<init>",
                "()V",
                false)  // 入棧后調(diào)用 HashMap 的構(gòu)造方法得到 HashMap 的實(shí)例
        methodVisitor.visitVarInsn(ASTORE, 0)   // 將 map 保存起來

        // 向匯總映射表中裝入所有子工程生成的映射表
        allMappingNames.each {
            methodVisitor.visitVarInsn(ALOAD, 0)
            methodVisitor.visitMethodInsn(INVOKESTATIC,
                    "com/example/router/mapping/$it",
                    "get",
                    "()Ljava/util/Map;",
                    false)
            methodVisitor.visitMethodInsn(INVOKEINTERFACE,
                    "java/util/Map",
                    "putAll",
                    "(Ljava/util/Map;)V",
                    true)
        }
        methodVisitor.visitVarInsn(ALOAD, 0)
        methodVisitor.visitInsn(ARETURN)
        methodVisitor.visitMaxs(2, 1)

        methodVisitor.visitEnd()
        classWriter.visitEnd()

        return classWriter.toByteArray();
    }

}

在完成生成字節(jié)碼的編碼之后谋右,接下來我們就要將生成的字節(jié)碼寫入 class 文件。所以回到 RouterMappingTransform.groovy 文件碌上,編寫以下代碼:

@Override
void transform(TransformInvocation transformInvocation)
        throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation)
    ...
    // 將生成的字節(jié)碼寫入文件
    File mappingJarFile = transformInvocation.outputProvider
            .getContentLocation(
                    "router_mapping",
                    getOutputTypes(),
                    getScopes(),
                    Format.JAR
            )   // 得到即將生成的 jar 包存放的位置
    println(getName() + " mappingJarFile = " + mappingJarFile)
    if (!mappingJarFile.getParentFile().exists()) {
        mappingJarFile.getParentFile().mkdirs()
    }
    if (mappingJarFile.exists()) {
        mappingJarFile.delete()
    }
    FileOutputStream fileOutPutStream = new FileOutputStream(mappingJarFile)
    JarOutputStream jarOutputStream = new JarOutputStream(fileOutPutStream)
    ZipEntry zipEntry = new ZipEntry(RouterMappingBytecodeBuilder.CLASS_NAME + ".class")
    jarOutputStream.putNextEntry(zipEntry)
    jarOutputStream.write(RouterMappingBytecodeBuilder.get(collector.mappingClassNames))
    jarOutputStream.closeEntry()
    jarOutputStream.close()
    fileOutPutStream.close()
}

最后 clean 后再編譯工程倚评,輸出以下日志:


字節(jié)碼編碼日志.png

然后再對(duì)應(yīng)的目錄下可以查看到 45.jar,解壓該 jar 包馏予,可以看到生成的 class 文件:


編譯目錄查看字節(jié)碼編碼結(jié)果.png

在編譯生成的 apk 文件中也能看到生成的 RouterMapping 文件:


apk 中查看字節(jié)碼編碼結(jié)果.png

頁面路由——打開頁面

最后需要完成的主要功能就是設(shè)計(jì)接口天梧,讓應(yīng)用在運(yùn)行期間通過傳入 url 在映射文件中查找對(duì)應(yīng)類名,執(zhí)行打開對(duì)應(yīng)頁面操作霞丧。
首先呢岗,肯定需要建立一個(gè)子工程,用于開發(fā)相關(guān)的代碼。
然后后豫,因?yàn)楫?dāng)應(yīng)用運(yùn)行時(shí)悉尾,我們需要把在編譯期生成好的頁面映射加載到內(nèi)存中。所以需要提供相應(yīng)的 init 方法挫酿。
接下來构眯,就是開發(fā)路由接口,在應(yīng)用運(yùn)行時(shí)等待傳入 url早龟,然后再對(duì) url 進(jìn)行匹配惫霸。
最后,實(shí)現(xiàn)打開 Activity 跳轉(zhuǎn)到相應(yīng)的頁面的邏輯葱弟。
當(dāng)然壹店,也可以擴(kuò)展一些跳轉(zhuǎn) Fragment、跳轉(zhuǎn)攜帶參數(shù)芝加、路由攔截的功能硅卢。

1、創(chuàng)建 router-api 工程藏杖,編寫初始化代碼:

public class Router {

    private static final String TAG = "Router";

    // 編譯期間生成的總映射表
    private static final String GENERATED_MAPPING = "com.example.router.mapping.RouterMapping";

    // 存儲(chǔ)所有映射表信息
    private static Map<String, String> mapping = new HashMap<>();

    public static void init() {
        // 反射獲取 GENERATED_MAPPING 類的 get() 方法
        try {
            Class<?> clazz = Class.forName(GENERATED_MAPPING);
            Method getMethod = clazz.getMethod("get");
            Map<String, String> allMapping = (Map<String, String>) getMethod.invoke(null);
            if (allMapping != null && !allMapping.isEmpty()) {
                Log.i(TAG, "init: get all mapping");
                Set<Map.Entry<String, String>> entrySet = allMapping.entrySet();
                for (Map.Entry<String, String> entry : entrySet) {
                    Log.i(TAG, "mapping: key = " + entry.getKey() + ", value = " + entry.getValue());
                }
                mapping.putAll(allMapping);
            }
        } catch (ClassNotFoundException e) {
            Log.e(TAG, "init called: " + e);
        } catch (NoSuchMethodException e) {
            Log.e(TAG, "init called: " + e);
        } catch (IllegalAccessException e) {
            Log.e(TAG, "init called: " + e);
        } catch (InvocationTargetException e) {
            Log.e(TAG, "init called: " + e);
        }
    }
}

然后在應(yīng)用中初始化:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Router.init();
    }
}

編譯驗(yàn)證有以下日志輸出:

2021-08-19 11:28:01.347 17799-17799/com.example.routerdemo I/Router: init: get all mapping
2021-08-19 11:28:01.347 17799-17799/com.example.routerdemo I/Router: mapping: key = /app/second, value = com.example.bm_a.SecondActivity
2021-08-19 11:28:01.347 17799-17799/com.example.routerdemo I/Router: mapping: key = /app/third, value = com.example.bm_b.ThirdActivity
2021-08-19 11:28:01.347 17799-17799/com.example.routerdemo I/Router: mapping: key = /app/first, value = com.example.bm_a.FirstActivity

2将塑、實(shí)現(xiàn) url 的匹配和打卡頁面。

public static void navigation(Context context, String url) {
    if (context == null || TextUtils.isEmpty(url)) {
        Log.i(TAG, "navigation called: param error");
        return;
    }
    // 1蝌麸、匹配 url抬旺,找到目標(biāo)頁面
    Uri uri = Uri.parse(url);
    String scheme = uri.getScheme();
    String host = uri.getHost();
    String path = uri.getPath();

    String targetActivityClass = "";
    Set<Map.Entry<String, String>> entries = mapping.entrySet();
    for (Map.Entry<String, String> entry : entries) {
        Uri sUri = Uri.parse(entry.getKey());
        String sScheme = sUri.getScheme();
        String sHost = sUri.getHost();
        String sPath = sUri.getPath();

        if (TextUtils.equals(scheme, sScheme)
                && TextUtils.equals(host, sHost)
                && TextUtils.equals(path, sPath)) {
            targetActivityClass = entry.getValue();
        }
    }

    if (TextUtils.isEmpty(targetActivityClass)) {
        Log.i(TAG, "navigation called: no destination found");
        return;
    }

    // 2、打開對(duì)應(yīng)頁面
    try {
        Class<?> clazz = Class.forName(targetActivityClass);
        Intent intent = new Intent(context, clazz);
        context.startActivity(intent);
    } catch (ClassNotFoundException e) {
        Log.e(TAG, "navigation called: " + e);
    }
}

在工程中驗(yàn)證:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_first);

    findViewById(R.id.button1).setOnClickListener(v ->
            Router.navigation(FirstActivity.this, "router://example.com/app/second"));

    findViewById(R.id.button2).setOnClickListener(v ->
            Router.navigation(FirstActivity.this, "router://example.com/app/third"));
}

總結(jié)

本文主要分享了組件化頁面路由框架的核心實(shí)現(xiàn)思路祥楣,并在 Demo 中實(shí)現(xiàn)了路由功能的基本邏輯。在這個(gè)過程中汉柒,觸及到了 APT误褪、字節(jié)碼插樁、Gradle 插件開發(fā)等各個(gè)知識(shí)點(diǎn)碾褂,實(shí)際上本次分享中對(duì)這些技術(shù)的介紹都還只是簡(jiǎn)單的應(yīng)用兽间。在實(shí)際項(xiàng)目過程中,還是推薦使用 ARouter 等成熟的框架正塌。不過在實(shí)際工作中嘀略,通過對(duì) APT、字節(jié)碼插樁乓诽、Gradle 插件等技術(shù)的簡(jiǎn)單了解帜羊,能為一些問題或方案設(shè)計(jì)提供更多的思路。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸠天,一起剝皮案震驚了整個(gè)濱河市讼育,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖奶段,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饥瓷,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡痹籍,警方通過查閱死者的電腦和手機(jī)呢铆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蹲缠,“玉大人棺克,你說我怎么就攤上這事『鹕埃” “怎么了逆航?”我有些...
    開封第一講書人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)渔肩。 經(jīng)常有香客問我因俐,道長(zhǎng),這世上最難降的妖魔是什么周偎? 我笑而不...
    開封第一講書人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任抹剩,我火速辦了婚禮,結(jié)果婚禮上蓉坎,老公的妹妹穿的比我還像新娘澳眷。我一直安慰自己,他們只是感情好蛉艾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開白布钳踊。 她就那樣靜靜地躺著,像睡著了一般勿侯。 火紅的嫁衣襯著肌膚如雪拓瞪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評(píng)論 1 305
  • 那天助琐,我揣著相機(jī)與錄音祭埂,去河邊找鬼。 笑死兵钮,一個(gè)胖子當(dāng)著我的面吹牛蛆橡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播掘譬,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼泰演,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了葱轩?” 一聲冷哼從身側(cè)響起粥血,我...
    開封第一講書人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤柏锄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后复亏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體趾娃,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年缔御,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抬闷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡耕突,死狀恐怖笤成,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情眷茁,我是刑警寧澤炕泳,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站上祈,受9級(jí)特大地震影響培遵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜登刺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一籽腕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧纸俭,春花似錦皇耗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缓呛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留放妈,地道東北人北救。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像芜抒,于是被迫代替她去往敵國和親珍策。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355

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