本文 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)證:
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 以后重新編譯工程异剥,可以看到下面的日志:
生成匯總映射表
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é)碼文件。
如下圖:
選擇 ASMified Tab 選項(xiàng)卡宗苍,可以看到工具幫助我們生成的編寫字節(jié)碼的 java 代碼稼稿。
下面就可以開始參考工具生成的代碼開始編寫 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 后再編譯工程倚评,輸出以下日志:
然后再對(duì)應(yīng)的目錄下可以查看到 45.jar,解壓該 jar 包馏予,可以看到生成的 class 文件:
在編譯生成的 apk 文件中也能看到生成的 RouterMapping 文件:
頁面路由——打開頁面
最后需要完成的主要功能就是設(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ì)提供更多的思路。