此文主要參考慕課網(wǎng)視頻度帮,視頻名如標(biāo)題。同時(shí)也加了一些視頻中沒有的操作处嫌,比如javapoet框架的使用。
第一章斟湃、構(gòu)建的基石Gradle
1.Gradle工程結(jié)構(gòu)
定義
Gradle是一個(gè)基于Apache和Apache Maven概念的項(xiàng)目自動(dòng)化構(gòu)建工具熏迹。它使用一種基于Groovy的特定領(lǐng)域語言來聲明項(xiàng)目設(shè)置,而不是傳統(tǒng)的XML凝赛。
APK構(gòu)建流程:
Gradle的安裝
有兩種安裝方式:
①在系統(tǒng)全局安裝Gradle
就是常規(guī)的安裝Gradle注暗,網(wǎng)上教程一大堆,沒有什么好提的
②生成Gradle Wrapper【劃重點(diǎn)】
有些時(shí)候從網(wǎng)上down下來帶Gradle項(xiàng)目墓猎,自己項(xiàng)目上又沒有安裝對(duì)應(yīng)版本的Gradle,再去安裝就很麻煩(深有同感)捆昏。這時(shí)候就體現(xiàn)出這種方式的便捷性了。
android項(xiàng)目創(chuàng)建的時(shí)候毙沾,會(huì)幫我們自動(dòng)生成Gradle Wrapper骗卜,也就是.gradlew文件。
Tips:如果沒有wrapper,那么可以cd到項(xiàng)目根目錄寇仓,執(zhí)行命令gradle wrapper
举户,就會(huì)生成Gradle Wrapper.
Gradle的執(zhí)行
學(xué)習(xí)Gradle的入門操作,肯定就是執(zhí)行Gradle自帶的一些命令了:
Gradle命令格式:
./gradlew [task-name...] [-option-name]
下面就介紹幾個(gè)Gradle自帶常用的命令:
清理Build緩存:./gradlew clean
查看所有子工程:./gradlew projects
查看所有任務(wù): ./gradlew tasks
其中遍烦,我們用clean命令測(cè)試demo的時(shí)候俭嘁,可以用加個(gè)option:
./gradlew clean -q
這是就只會(huì)輸出我們的println信息了,不會(huì)有系統(tǒng)信息干擾視線
Gradle升級(jí)的兩種方式
..
Gradle腳本基礎(chǔ)
有三種gradle文件需要了解:
①setting.gradle: 項(xiàng)目包含哪些子工程
②build.gradle:一個(gè)是根目錄下服猪,應(yīng)用于所有子項(xiàng)目可以共用的配置供填;另一塊是每個(gè)android library里的,是每個(gè)子項(xiàng)目的配置信息罢猪。
③gradle.properties: 在工程根目錄下近她,配置一些開關(guān)型參數(shù)。
Gradle生命周期
1.初始化階段
gradle支持單個(gè)工程或多個(gè)工程的編譯坡脐。
兩件事:
** ①判斷需要參與編譯的子工程赐俗,為這些子工程創(chuàng)建一個(gè)Project對(duì)象檩电。
②創(chuàng)建Settings對(duì)象,并在其上執(zhí)行settings.gradle腳本挡篓,建立工程之間的層次結(jié)構(gòu) **
2.配置階段
兩件事:
①會(huì)在每個(gè)Project對(duì)象上執(zhí)行對(duì)應(yīng)的build.gradle文件捅暴,完成對(duì)Project的配置恬砂。
⑥并且根據(jù)項(xiàng)目的配置,構(gòu)建出一個(gè)任務(wù)關(guān)系依賴圖蓬痒,為執(zhí)行階段做準(zhǔn)備泻骤。
3.執(zhí)行階段
就一件事:
** 判斷哪些task需要執(zhí)行,并執(zhí)行對(duì)應(yīng)的task **
此外梧奢,聲明周期的監(jiān)聽實(shí)現(xiàn)如下:
//## gradle生命周期回調(diào)
gradle.addBuildListener(new BuildAdapter() {
@Override
void settingsEvaluated(Settings settings) {
super.settingsEvaluated(settings)
println("[life-cycle] 初始化階段完成")
}
@Override
void projectsEvaluated(Gradle gradle) {
super.projectsEvaluated(gradle)
println("[life-cycle] 配置階段完成")
}
@Override
void buildFinished(BuildResult result) {
super.buildFinished(result)
println("[life-cycle] 構(gòu)建結(jié)束")
}
})
gradle中幾個(gè)主要角色
主要有三個(gè)角色:
初始化階段 - root project
配置階段 - project
執(zhí)行階段 - task
Task
Gradle 的構(gòu)建工作都是由一系列 Task 組合完成的狱掂。一個(gè) Project 里面可以包含很多個(gè) Task 。Task 可以理解為一個(gè)執(zhí)行體亲轨,在 Project 的視角下趋惨,也可以看作是一個(gè)原子性的操作。
gradle中核心的工作單元就是一個(gè)又一個(gè)的task.創(chuàng)建Task有多種方式:
//創(chuàng)建方式一: 任務(wù)名字 方式創(chuàng)建Task
def customTask0 = task('customTask0')
customTask0.doLast {
println("通過任務(wù)名字方式創(chuàng)建Task")
}
//創(chuàng)建方式二: 任務(wù)名字 + 一個(gè)配置Map 創(chuàng)建Task
def customTask1 = task(group: 'RyeDemoTasks', 'customTask1')
customTask1.doLast {
println("通過 任務(wù)名字 + 一個(gè)配置Map 創(chuàng)建Task")
}
//創(chuàng)建方式三: 任務(wù)名 + 閉包
task customTask2(group: 'RyeDemoTasks', description: '任務(wù)名+閉包方式創(chuàng)建任務(wù)') {
println('帶閉包的創(chuàng)建task')
}
//創(chuàng)建方式四: 通過TaskContainer創(chuàng)建任務(wù)
tasks.create('customTask3') {
group 'RyeDemoTask'
description '通過TaskContainer創(chuàng)建任務(wù)'
doLast {
println('hello~')
}
}
第三章 頁面路由開發(fā)實(shí)戰(zhàn)Gradle插件【路由框架】
Gradle插件
Gradle插件主要分為兩種類型:
1.二進(jìn)制插件
二進(jìn)制插件就是實(shí)現(xiàn)了 org.gradle.api.Plugin 接口的插件惦蚊,每個(gè) Java Gradle 插件都有一個(gè) plugin id器虾,可以通過如下方式使用一個(gè) Java 插件:
apply plugin : 'java'
其中 java 是 Java 插件的 plugin id,對(duì)于 Gradle 自帶的核心插件都有唯一的 plugin id.對(duì)外會(huì)搞成一個(gè)jar包蹦锋。
2.腳本插件
腳本插件的使用實(shí)際上就是某個(gè)腳本文件的使用兆沙,使用腳本插件時(shí)將腳本加載進(jìn)來就可以了,使用腳本插件要使用到關(guān)鍵字 from莉掂,后面的腳本文件可以是本地的也可以是網(wǎng)絡(luò)上的腳本文件葛圃。腳本插件更輕量。
兩種插件使用的簡單介紹:
自定義二進(jìn)制插件
常規(guī)流程,三步走:
① 聲明插件ID與版本號(hào)
② 在具體的子工程中應(yīng)用插件 : apply plugin : 'pluginName'
③ 配置插件
自定義腳本插件
①創(chuàng)建gradle文件
②在子工程中(或者根工程中)引用此gradle文件:
apply from: 'xxxx.gradle'
ex:
順帶說一下這個(gè)apply()方法:
此方法在
PluginAware
中聲明:可以接收三種不同類型的參數(shù):
//閉包作為參數(shù)
void apply(Closure closure)
//配置一個(gè)ObjectConfigurationAction
void apply(Action<? super ObjectConfigurationAction> action);
//Map作為參數(shù)
void apply(Map<String, ?> options);
實(shí)例:
//Map作為參數(shù)
apply plugin:'java'
//閉包作為一個(gè)參數(shù)
apply {
plugin 'java'
}
//配置一個(gè)ObjectConfigurationAction
apply(new Action<ObjectConfigurationAction>() {
@Override
void execute(ObjectConfigurationAction objectConfigurationAction) {
objectConfigurationAction.plugin('java')
}
})
Gradle插件開發(fā)流程
1.建立插件工程
2.實(shí)現(xiàn)插件內(nèi)部邏輯
3.發(fā)布與使用插件
頁面路由
功能梳理
1.標(biāo)記頁面(標(biāo)記出URL與頁面的對(duì)應(yīng)關(guān)系)
2.收集頁面(收集映射關(guān)系装悲,統(tǒng)一記錄進(jìn)映射關(guān)系表昏鹃,這樣才可以根據(jù)映射關(guān)系表打開對(duì)應(yīng)頁面)
3.生成文檔(頁面多,生成統(tǒng)一文檔诀诊,記錄URL與頁面的對(duì)應(yīng)關(guān)系洞渤;方便查詢)
4.注冊(cè)映射(將所有的路由表映射關(guān)系注冊(cè)到路由框架中)
5.打開頁面(根據(jù)URL打開對(duì)應(yīng)的頁面)
前四個(gè)在編譯期間完成。
實(shí)戰(zhàn)一 插件工程建立
二進(jìn)制插件的實(shí)現(xiàn)方式有兩種:
①一個(gè)獨(dú)立工程:如果需要調(diào)試属瓣,需要手動(dòng)發(fā)布成一個(gè)二進(jìn)制插件的jar包载迄。
這樣其他工程才能引用,才可以測(cè)試抡蛙,測(cè)試起來會(huì)比價(jià)麻煩
②建立buildSrc子工程:在構(gòu)建的時(shí)候护昧,gradle就會(huì)自動(dòng)的將其打包成一個(gè)二進(jìn)制jar包,更加方便粗截。
發(fā)布插件有兩種方向:
①發(fā)布到本地倉庫
②發(fā)布到遠(yuǎn)程倉庫
所以我們這里使用第二種方式開啟我們的插件之旅:
步驟如下:
1.建立buildSrc子工程
①在根目錄下創(chuàng)建buildSrc包(名字必須為此惋耙,gradle規(guī)定);
②創(chuàng)建build.gradle文件
③在上面文件中添加基礎(chǔ)配置
2.建立插件運(yùn)行入口
①在buildSrc工程下創(chuàng)建源碼存放目錄:
src/main/groovy
在這個(gè)文件夾中存放源碼熊昌。
②上面那個(gè)目錄是規(guī)定的绽榛,還需要?jiǎng)?chuàng)建我們自己的包名,ex:
com/imooc/router/gradle
如圖所示:
3.新建插件文件
創(chuàng)建groovy插件文件婿屹,需要實(shí)現(xiàn)Plugin接口
package com.dream.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
class RouterPlugin implements Plugin<Project>{
//實(shí)現(xiàn)apply方法灭美,注入插件的邏輯
@Override
void apply(Project project) {
println("I\'m from RouterPlugin,apply from ${project.name}")
}
}
4.創(chuàng)建屬性文件(指定plugin名稱)
【這里properties文件的名字:com.rye.router就是我們插件的id!昂利!】apply plugin的時(shí)候用的就是這個(gè)届腐。
目前只指定插件實(shí)現(xiàn)文件路徑:
implementation-class=com.dream.gradle.RouterPlugin
5.實(shí)現(xiàn)參數(shù)配置
有以下幾個(gè)步驟
①定義Extension
實(shí)際上就是定義一個(gè)實(shí)體類,指定擴(kuò)展的各種屬性:
package com.dream.gradle
class RouterExtension {
String wikiDir
}
定義的話蜂奸,就這么簡單
②注冊(cè)Extension
注冊(cè)Extension需要在我們的Plugin文件中注冊(cè):
//注冊(cè)Extension
project.getExtensions().create("router", RouterExtension)
傳入兩個(gè)參數(shù)犁苏,一個(gè)是我們定義的擴(kuò)展名字,這個(gè)隨便起扩所。另一個(gè)就是我們剛才創(chuàng)建的擴(kuò)展實(shí)體類
③使用Extenstion
在我們引入插件的子工程中就可以運(yùn)用我們剛才定義的插件的擴(kuò)展屬性了:
這樣我們就在我們子工程里的gradle中配置好了我們自定義的擴(kuò)展屬性傀顾,配置好了,那我們就需要拿到這個(gè)配置的屬性執(zhí)行我們插件的邏輯了碌奉,也就到了下一步
④獲取Extension
在我們插件中短曾,需要等配置完成了,才能拿到我們上面設(shè)置的屬性
執(zhí)行g(shù)radlew clean -q 就可以看到我們配置之后屬性獲取到了:
發(fā)布與使用插件
畢竟插件是要給別人一起用的赐劣,所以我們需要將插件打成jar包嫉拐,發(fā)布出去,和團(tuán)隊(duì)其他人一起使用魁兼。發(fā)布兩種方式:發(fā)布到遠(yuǎn)程倉庫或發(fā)布到本地倉庫婉徘。
這里我們通過發(fā)布到本地倉庫來了解這個(gè)流程:
發(fā)布到本地倉庫
在工程中應(yīng)用插件
發(fā)布插件到本地倉庫
①在buildSrc的build.gradle文件夾下編寫我們的上傳任務(wù):
uploadArchives(這個(gè)名字是固定的,這個(gè)任務(wù)已經(jīng)被刪除了,以后再寫就用maven-publish或者ivy-publish任務(wù))
②創(chuàng)建插件文件夾
【警告警告8呛簟儒鹿!】有一點(diǎn)我們必須明確:在buildSrc中不能直接發(fā)布插件!<肝睢T佳住!蟹瘾。所以我們需要將buildSrc拷貝一份到當(dāng)前項(xiàng)目的根目錄下圾浅,作為插件發(fā)布的源工程。
在mac下的命令是
cp -rf buildSrc router-gradle-plugin
在windows下的命令是
xcopy buildSrc router-gradle-plugin /E
拷貝完成后憾朴,項(xiàng)目識(shí)別不到這是一個(gè)module,所以還需要在settings.gradle中include進(jìn)來:
③生成本地maven倉庫
執(zhí)行我們?cè)诓寮uild.gradle中定義的uploadArchives命令:
gradlew :router-gradle-plugin:uploadArchives
之后就會(huì)生成我們本地的mave倉庫:
發(fā)布后:
這里的包名對(duì)應(yīng)的就是groupId狸捕,文件夾名對(duì)應(yīng)的是artifactId;版本好在這個(gè)文件夾下:
可以看到我們的插件工程正確的上傳到了本地倉庫中!众雷!
【插件上傳倉庫成功>呐摹!砾省!】
??ヽ(°▽°)ノ?撒花??ヽ(°▽°)ノ?
這個(gè)repo文件夾就是我們剛才在build.gradle中指定的../repo路徑鸡岗。
可以看到其中的jar包就是我們發(fā)布出后的插件。
至此纯蛾,我們的插件已經(jīng)在本地倉庫上傳成功纤房。其他本地項(xiàng)目就可以使用此插件了纵隔》撸可以新建一個(gè)項(xiàng)目測(cè)試,這里之前有測(cè)試項(xiàng)目捌刮,我就用之前的項(xiàng)目做演示碰煌。
引用插件
其他項(xiàng)目引用插件可分為四步:
①在項(xiàng)目的build.gradle文件中配置Maven倉庫地址;
需要分別在buildscript閉包下的repositories和allprojects閉包下的repositories中引入倉庫地址:
/**
* 1.配置Maven倉庫地址绅作;這里可以是相對(duì)路徑
*/
maven {
url uri("/Users/zhaozhenguo/Desktop/projects/AndroidZex/repo")
}
②聲明依賴的插件
還是在此文件中芦圾,buildscript閉包下的dependencies中聲明:
/**
* 2.聲明依賴的插件
* 格式--> groupId : artifactId : version
*/
classpath 'com.rye.router:router-gradle-plugin:1.0.0'
③應(yīng)用路由插件
在需要插件的子模塊中引入插件:
/**
* 3.應(yīng)用路由插件
*/
apply plugin: 'com.rye.router'
④向插件中傳遞參數(shù)
/**
* 4.向路由插件傳遞參數(shù)
*/
router {
wikiDir getRootDir().absolutePath
}
就這四步走戰(zhàn)略~
引入并配置完成后,在此項(xiàng)目中運(yùn)行
./gradlew clean -q
檢驗(yàn)是否導(dǎo)入成功:
可以看出俄认,本地倉庫導(dǎo)入成功个少。
至于如何發(fā)布到遠(yuǎn)程倉庫,等我們此插件編寫完畢后眯杏,再進(jìn)行嘗試~
第四章 頁面路由開發(fā)實(shí)戰(zhàn)- APT采集頁面路由信息
本章介紹:
APT
注解處理器相關(guān)基礎(chǔ)
注解相關(guān)-包含APT
參考資料
APT工作原理--面試必備夜焦!
頁面路由開發(fā)-功能梳理
頁面路由開發(fā)所需功能主要有以下幾點(diǎn):
1.標(biāo)記頁面(標(biāo)記出URL與頁面的對(duì)應(yīng)關(guān)系)
2.收集頁面(收集映射關(guān)系,統(tǒng)一記錄進(jìn)映射關(guān)系表岂贩,這樣才可以根據(jù)映射關(guān)系表打開對(duì)應(yīng)頁面)
3.生成文檔(頁面多茫经,生成統(tǒng)一文檔,記錄URL與頁面的對(duì)應(yīng)關(guān)系;方便查詢)
4.注冊(cè)映射(將所有的路由表映射關(guān)系注冊(cè)到路由框架中)
5.打開頁面(根據(jù)URL打開對(duì)應(yīng)的頁面)
采集頁面信息:
定義注解:@Route 【用來標(biāo)記頁面】
采集注解:實(shí)現(xiàn) RouteProcessor
發(fā)布與使用 :新建META-INF文件夾卸伞,注冊(cè)Processor【可以手動(dòng)注冊(cè)抹镊,也可以采用谷歌官方框架auto,自動(dòng)幫我們注冊(cè)Processor】
新建的注解處理器工程有兩個(gè)重要的組成部分:
①M(fèi)ETA-INF:此目錄下會(huì)配置一個(gè)配置文件,此配置文件會(huì)有一個(gè)入口荤傲,入口指向我們的注解處理器
②注解處理器:將會(huì)接受javac幫我們找到的所有注解垮耳,然后在其中進(jìn)行一些自定義操作,比如生成源碼文件等弃酌。
【注解工程建立】
1.建立注解工程
新建Directory氨菇,名稱router_annotation:
2.添加build.gradle文件(并引入java插件)
3.include 注解工程
在settings.gradle中將注解工程添加到編譯中:
4.定義注解
這里的文件目錄也需要我們自己新建。
這里注解的生命周期只需要保存在編譯期就行妓湘。注解的類型在介紹APT的參考資料中已經(jīng)做過總結(jié)查蓉,這里暫不贅述。
注解內(nèi)容就兩個(gè):
value代表當(dāng)前的頁面的URL榜贴,description是對(duì)當(dāng)前頁面的中文描述豌研;
5.使用注解
①在子工程中依賴router_annotation工程。
②在子工程中任意一個(gè)類中使用此注解:
【注解處理器工程建立】
注解處理器前期處理流程如下:
1.新建Directory唬党,名稱router_processor
2.新建build.gradle
①引入java插件(這里也引入了kotlin)
②依賴注解工程
3.include 注解處理器工程
4.采集注解
①創(chuàng)建源碼目錄及包目錄
②新建Processor【注解處理器里的操作可以說是路由框架的核心所在了】
新建一個(gè)類RouteProcessor繼承自AbstractProcessor换薄。
然后可以通過重寫getSupportAnnotationTypes方法告訴編譯器當(dāng)前處理器支持的注解類型魏烫。這里采用了@SupportAnnotationTypes注解來指定我們要處理的注解。這個(gè)必須指定,否則返回空集合塞蹭。**
getSupportedAnnotationTypes和getSupportedSourceVersion這兩個(gè)方法,也可以使用注解標(biāo)識(shí)敛纲,如下所示耘眨,如果兩個(gè)地方都寫,以該方法的結(jié)果為主税迷。
所以我們這里還加了一個(gè)指定源碼版本的注解永丝,我這邊用的是JAVA1.8,課程中的是JAVA1.7.不同版本的會(huì)有坑。這里需要留意一下箭养。
還有一個(gè)抽象方法必須重寫:process(~)方法
編譯器找到要處理的注解后慕嚷,會(huì)回調(diào)此方法”厦冢【十分十分重要的方法喝检!】
【開始實(shí)現(xiàn)注解處理器的process方法】
process方法有兩個(gè)參數(shù):
①第一個(gè)參數(shù)是一個(gè)TypeElement 的Set集合,將返回所有的由該P(yáng)rocessor處理撼泛,并待處理的 Annotations的類的集合挠说。
②第二個(gè)參數(shù)RoundEnvironment.表示當(dāng)前或是之前的【運(yùn)行環(huán)境】,可以通過該對(duì)象查找找到的注解坎弯。(getElementsAnnotatedWith)
我們process方法處理的第一步就是拿到所有的@Route注解的信息纺涤,后面就可以通過這些信息建立映射關(guān)系译暂。
這里簡單分析一下,上面是【如何獲取注解的信息】的撩炊。
1.通過roundEnvironment的getElementsAnnotatedWith方法傳入@Route的泛型外永,拿到所有@Route注解的類的Set<Element>集合
2.遍歷Set<Element>集合,將Element強(qiáng)轉(zhuǎn)成typeElement拧咳,并通過typeElement的getAnnotation(Class<A> var1)方法伯顶,獲取到當(dāng)前Element的注解。
3.拿到@Route注解的value骆膝、descrption信息祭衩。通過typeElement.getQualifiedName().toString()方法拿到當(dāng)前類的真實(shí)路徑信息。
至于拿到這些信息后存儲(chǔ)到本地是我們之后要說的事了阅签。
這里我們加了sout來輸出我們拿到的注解的信息掐暮,當(dāng)時(shí)代碼執(zhí)行不到這個(gè)地方!因?yàn)檫€沒有【注冊(cè)注解處理器U印路克!】
注冊(cè)注解處理器
1.可以使用AutoService庫來注冊(cè)注解處理器.AutoService這里主要是用來生成
META-INF/services/javax.annotation.processing.Processor文件的。
在router_processor的build.gradle中引入autoService的依賴:
2.在RouteProcessor上聲明@AutoService(Processor.class)
3.在我們的子工程中使用processor(課程里用的是app module养交,也就是主module精算,我這邊用的是自己建的子module)
4.驗(yàn)證:
①先執(zhí)行clean清楚build殘留;
②執(zhí)行
./gradlew :app:assembleDebug -q
因?yàn)槲沂欠旁趜gstep子模塊中來驗(yàn)證注解處理器的使用的碎连,所以執(zhí)行的命令是:
./graldew :zgstep:assembleDebug -q
之后可以看到日志信息:
說明我們的【注解處理器注冊(cè)成功】
看看我們AutoService生成的META-INF目錄在哪:
這里需要注意灰羽,如果我們子module中用的是annotationProcessor引入的注解處理器,那么META-INF目錄是在router-processor->build->classes->java->META-INF.
但我們用的是kotlin的kapt引入的注解處理器鱼辙,META-INF目錄文件路徑是:
tmp->kapt3->classes->META-INF廉嚼。
至于META-INF文件的作用,上面已經(jīng)說了座每,必須搞懂前鹅。那個(gè)參考文章必須要看摘悴!面試要扯到APT峭梳,必問!
這里補(bǔ)充一下蹂喻,在編譯過程中葱椭,注解處理器的process方法會(huì)調(diào)用多次,為了避免邏輯重復(fù)執(zhí)行口四,我們需要新增一個(gè)判斷:
生成類-類信息拼接(路由表)
我們需要使用注解處理器生成一個(gè)路由信息表的類孵运!
確定路由表的結(jié)構(gòu):
實(shí)際上就是一個(gè)類中提供一個(gè)靜態(tài)的方法,用于獲取路由表蔓彩。
這個(gè)方法中持有了一個(gè)Map<String,String>,key是頁面的URL治笨,value是類的真實(shí)路徑驳概。
這了動(dòng)態(tài)的部分就是類名、put操作旷赖,其他部分的代碼都是固定
生成類信息可以用字符串拼接顺又,也可以使用javapoet來生成。
我們需要生成路由表信息等孵,跳轉(zhuǎn)的時(shí)候稚照,路由會(huì)在路由表中查找對(duì)應(yīng)的跳轉(zhuǎn)頁面進(jìn)行跳轉(zhuǎn)。
這個(gè)路由表最簡單的形式俯萌,起碼得有一個(gè)HashMap果录,key是路由路徑,value是要跳轉(zhuǎn)頁面的的真實(shí)路徑咐熙。ex:
public class RouterMapping_1613657433732 {
public static Map<String, String> get() {
Map<String, String> mapping = new HashMap();
mapping.put("route://page_content","com.rye.catcher.activity.ContentProviderActivity");
return mapping;
}
}
如果通過StringBuilder拼接生成弱恒,就直接生成得了。這里看一下用javapoet如何生成此類:
private ArrayList<RouterValue> routerValues;
private void buildMapping() {
ClassName mapClassName = ClassName.get(Map.class);
MethodSpec get = MethodSpec.methodBuilder("get")
.returns(ParameterizedTypeName.get(mapClassName, ClassName.get(String.class), ClassName.get(String.class)))
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addCode(getCodeBlock(mapClassName))
.build();
TypeSpec routerMappingClass = TypeSpec.classBuilder("RouterMapping_"+System.currentTimeMillis())
.addMethod(get)
.addModifiers(Modifier.PUBLIC)
.build();
JavaFile file = JavaFile.builder("com.dawn.zgstep.mapping", routerMappingClass)
.build();
try {
file.writeTo(processingEnv.getFiler());
hasCreatedFile = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
}
System.out.println(file);
}
private CodeBlock getCodeBlock(ClassName mapClassName) {
CodeBlock.Builder contentBlock = CodeBlock.builder().addStatement("$T mapping = new $T()",
ParameterizedTypeName.get(mapClassName, ClassName.get(String.class),
ClassName.get(String.class)), ClassName.get("java.util", "HashMap"));
for (RouterValue value : routerValues) {
contentBlock.addStatement("mapping.put($S,$S)", value.url, value.realPath);
}
return contentBlock.addStatement("return mapping").build();
}
//這里的routerValues存儲(chǔ)了對(duì)應(yīng)頁面的路由url和真實(shí)路徑:
寫完之后棋恼,重新調(diào)用一下:
./gradlew :app:assembleDebug
即可看到
在build文件夾下已經(jīng)生成了我們所需的路由表信息斤彼。
上傳插件并使用
-----------------------------這一段可都是滿滿的干貨:---------------------------
在本地調(diào)試正常后,我們就需要將插件上傳到倉庫中蘸泻,供其他項(xiàng)目使用了琉苇。
上傳可分為以下幾步:
1.指定插件基本信息,包括但不僅限于插件的ID悦施、URL并扇、 NAME
2.新建插件腳本文件
3.執(zhí)行腳本上傳任務(wù)
4.在其他項(xiàng)目中引用插件
我們一步一步來看:
新建插件腳本文件
新建一個(gè)maven-publish.gradle文件:
//使用maven插件中的發(fā)布功能
apply plugin: 'maven'
Properties gradleProperties = new Properties()
gradleProperties.load(project.rootProject.file('gradle.properties').newDataInputStream())
def VERSION_NAME = gradleProperties.getProperty("VERSION_NAME")
def POM_URL = gradleProperties.getProperty("POM_URL")
def GROUP_ID = gradleProperties.getProperty("GROUP_ID")
println("currentPlugin:versionName:$VERSION_NAME,pomUrl:$POM_URL,groupId:$GROUP_ID")
Properties projectGradleProperties = new Properties()
projectGradleProperties.load(
project.file('gradle.properties').newDataInputStream()
)
def POM_ARTIFACT_ID = projectGradleProperties.getProperty("POM_ARTIFACT_ID")
uploadArchives {
repositories {
mavenDeployer {
//填入發(fā)布信息
repository(url: uri(POM_URL)) {
pom.groupId = GROUP_ID
pom.artifactId = POM_ARTIFACT_ID
pom.version = VERSION_NAME
}
pom.whenConfigured { pom ->
pom.dependencies.forEach { dep ->
if (dep.getVersion() == "unspecified") {
dep.setGroupId(GROUP_ID)
dep.setVersion(VERSION_NAME)
}
}
}
}
}
}
這里有幾點(diǎn)要著重提一下:
①屬性配置
可以在根目錄或者子module的gradle.properties中設(shè)置屬性
這里我們要上傳倉庫的是兩個(gè)module:router_annotation和router_processor工程。
這兩個(gè)module都要指定對(duì)應(yīng)的GROUP_ID抡诞、VERSION_ NAME穷蛹、POM_URL(倉庫地址)以及POM_ARTIFACT_ID (插件的id).
然鵝,這兩個(gè)插件除了ID不同昼汗,前面三者都是可以一樣的肴熏,所以我們聲明在根目錄下的gradle.properties中。而POM_ARTIFACT_ID可以聲明在兩個(gè)module各自的gradle.properties中顷窒。因?yàn)檫@兩個(gè)項(xiàng)目創(chuàng)建的時(shí)候是java library.沒有g(shù)radle.properties蛙吏,所以需要我們手動(dòng)創(chuàng)建一下這個(gè)文件。然后指定插件ID即可鞋吉。ex:
②Properties
此類是java.util里的工具類鸦做,繼承自Hashtable。移鍵值對(duì)形式存儲(chǔ)屬性值谓着。主要方法如下:
這里我們主要使用此類的load方法泼诱,用來讀取根項(xiàng)目以及子項(xiàng)目中g(shù)radle.properties文件里設(shè)置的屬性。
這里讀取的屬性都是我們?cè)趃radle.properties里設(shè)置的赊锚。
最重要的還是這個(gè)uploadArchives任務(wù)治筒。
實(shí)際上發(fā)布腳本除了這個(gè)任務(wù)還有publishing
實(shí)際上Gradle 1.3之后就推出了publishing任務(wù)用戶上傳插件到maven倉庫屉栓。但是這都6.8.2了,uploadArchives依然在使用耸袜,而且 被作廢的那一天目前還看不到系瓢,加上課程中老師采用這種方式,所以目前就用uploadArchives來將我們的插件上傳到maven倉庫句灌。
因?yàn)椴寮玫氖莔aven的上傳功能夷陋,所以最上面需要apply maven插件。如果采用publishing任務(wù)上傳胰锌,需要使用的是maven-publish插件骗绕。
格式比較固定,暫不多說资昧〕晖粒【后續(xù)需要對(duì)上面中的閉包都做個(gè)基本了解,以及pom】
3.上傳插件到本地倉庫中
創(chuàng)建好插件后格带,就可以在需要上傳的module中的build.gradle中引用我們的發(fā)布插件了撤缴,ex:
因?yàn)槲沂欠旁诟夸浵碌腸onfig文件夾中,所以路徑可能有出入叽唱,按照自己的插件文件路徑來就行屈呕。
router_processor和router_annotation都需要上傳到maven倉庫,所以兩個(gè)module的build.gradle中都需要各自引入棺亭。
引入后虎眨,sync后,分別執(zhí)行命令:
:router_processor:uploadArchives
:router_annotation:uploadArchives
即可正確上傳我們的插件到倉庫中镶摘,可以在我們的本地倉庫../repo中看到我們上傳成功的插件:
4.其他項(xiàng)目引用插件
分為簡單的兩步:
①在根工程的build.gradle中指定mven倉庫地址
需要在buildScript和allprojects兩個(gè)閉包的repositories中均指定maven地址:
②在子工程的build.gradle中implementation和kapt
implementation 'com.rye.router:router-annotation:1.0.0'
annotationProcessor "com.rye.router:router-processor:1.0.0"
引入我們的插件嗽桩。
sync后build一下,就可以發(fā)現(xiàn)我們可以在別的倉庫中引導(dǎo)我們發(fā)布到倉庫中的插件凄敢,明顯的現(xiàn)象就是:
①可以使用@Route注解
②在build-generated-ap_generated_sources文件夾下找到我們路由表文件:
那么到目前為止碌冶,我們簡單的插件開發(fā),與上傳倉庫涝缝,以及在其他項(xiàng)目中使用就可以告一段落扑庞,下面既可以完善我們插件的工程了。
第五章 完善Gradle插件
前面我們已經(jīng)將頁面路由中的標(biāo)記頁面與收集頁面功能開發(fā)完畢了俊卤。這一章主要就是進(jìn)行第三步:生成文檔嫩挤。
邏輯梳理
生成文檔主要分為三步:
- 傳遞路徑參數(shù) (把文檔存儲(chǔ)到哪個(gè)地方)
- 生成JSON文件(注解處理器在查到注解的時(shí)候爽蝴,就可以把注解信息存儲(chǔ)到j(luò)son文件中枉阵;每一個(gè)json文件包含的是當(dāng)前子工程的映射信息)
- 匯總生成文檔 (在某個(gè)統(tǒng)一路徑下既峡,將所有子工程的json文件匯總,生成一個(gè)統(tǒng)一的文檔)
1.傳遞路徑參數(shù)
生成json文檔的位置我們希望是可以動(dòng)態(tài)指定的狠怨,一種比較好的方式是在build.gradle中給用戶提供一個(gè)入口约啊,指定生成json文檔的文件夾根目錄,ex:
image.png
這種方式其實(shí)不錯(cuò)佣赖,不過我們還可以配置在plugin中:
image.png
這種就相當(dāng)于在插件中動(dòng)態(tài)添加參數(shù)恰矩,更加方便,起碼用戶不用sync了...
2.生成json文件
生成json文件的邏輯實(shí)際上和我們生成路由表的邏輯是在一塊的憎蛤。畢竟都是獲取@Route的信息外傅,存儲(chǔ)到文件中:
沒什么好說的,而且存儲(chǔ)的方式是jsonArray俩檬,格式上不怎么好萎胰,后續(xù)需要修改。
3.生成匯總文檔
在生成匯總文檔前棚辽,我們還有個(gè)工作要做技竟,就是自動(dòng)清理構(gòu)建產(chǎn)物。每次build的時(shí)候屈藐,用戶的路由信息可能有所改變榔组,可以新增了一個(gè)@Route,也可能修改了一個(gè)@Route等。所以這就要求我們每次build的時(shí)候联逻,清理一下上次生成的每個(gè)模塊的json文件搓扯。
這塊的代碼我們?cè)赽uildSrc中的RouterPlugin中進(jìn)行處理,跟上面的自動(dòng)傳遞參數(shù)處理的地方一致:
接下來就是重頭戲了包归,生成匯總文檔:
首先明確生成時(shí)機(jī),肯定要在配置結(jié)束后擅编,開始執(zhí)行任務(wù)的時(shí)候。在javac任務(wù)結(jié)束后箫踩,注解處理器的任務(wù)執(zhí)行完畢爱态,生成了每個(gè)模塊的json文件,這時(shí)候我們就可以獲取到所有json文件里的路由信息境钟。那么就可以生成我們的匯總文檔了:
project.afterEvaluate { //配置結(jié)束锦担,可拿到用戶配置的參數(shù)
RouterExtension extension = project["router"]
println("用戶設(shè)置的wiki路徑為:${extension.wikiDir}")
buildMarkDown(project,extension.wikiDir)
}
//3.在javac任務(wù)[compileDebugJavaWithJavac]后匯總生成文檔(需要在配置結(jié)束后)
private void buildMarkDown(Project project,String wikiDir) {
project.tasks.findAll { task ->
task.name.startsWith('compile') && task.name.endsWith('JavaWithJavac')
}.each { task ->
task.doLast {
File routerMappingDir = new File(project.rootProject.projectDir, "router_mapping")
if (!routerMappingDir.exists()) {
return
}
File[] allChildFiles = routerMappingDir.listFiles()
if (allChildFiles.length < 1) {
return
}
StringBuilder markDownBuilder = new StringBuilder()
markDownBuilder.append("# 頁面文檔\n\n")
allChildFiles.each { child ->
if (child.name.endsWith(".json")) {
JsonSlurper jsonSlurper = new JsonSlurper()
def content = jsonSlurper.parse(child)
content.each { innerContent ->
def url = innerContent['url']
def description = innerContent['description']
def realPath = innerContent['realPath']
markDownBuilder.append("## $description \n")
markDownBuilder.append("-url: $url \n")
markDownBuilder.append("- realPath: $realPath \n\n")
}
}
}
File wikiFileDir = new File(wikiDir)
if (!wikiFileDir.exists()) {
wikiFileDir.mkdir()
}
File wikiFile = new File(wikiFileDir, "頁面文檔.md")
if (wikiFile.exists()) {
wikiFile.delete()
}
wikiFile.write(markDownBuilder.toString())
}
}
}
其他問題
1.JsonSlurper (Groovy里的類)
①JsonSlurper JsonSlurper是一個(gè)將JSON文本或閱讀器內(nèi)容解析為Groovy數(shù)據(jù)的類結(jié)構(gòu),例如map慨削,列表和原始類型洞渔,如整數(shù),雙精度缚态,布爾和字符串磁椒。
②JsonOutput 此方法負(fù)責(zé)將Groovy對(duì)象序列化為JSON字符串
ex:
def jsonSlurper = new JsonSlurper()
def object = jsonSlurper.parseText('{ "name": "John", "ID" : "1"}')
println(object.name)
println(object.ID)
結(jié)果輸出如下:
John
1
上面我們解析每一個(gè)json文件,就是使用JsonSlurper來解析成Groovy的類結(jié)構(gòu)玫芦。
- project.extensions.findByName('kapt')
image.png
如上圖浆熔,我點(diǎn)擊arguments的時(shí)候,發(fā)現(xiàn)根本點(diǎn)不了桥帆!
點(diǎn)開findByName方法医增,可以看到ExtenstionContainer返回的是一個(gè)Object對(duì)象:
image.png
后面通過打log發(fā)現(xiàn)返回的類的實(shí)例是:
image.png
也就是KaptExtension慎皱。
可以看一下這個(gè)類的源碼:
KaptExtension源碼
可以看到:
image.png
確實(shí)有個(gè)arguments方法,接收一個(gè)閉包參數(shù)叶骨。且 閉包里的內(nèi)容:
這也就解釋通了為什么可以如最上面那樣設(shè)置參數(shù)了茫多。
第六章 字節(jié)碼插樁
插樁
用通俗的話來講,插樁就是將一段代碼通過某種策略插入到另一段代碼忽刽,或替換另一段代碼天揖。這里的代碼可以分為源碼和字節(jié)碼,而我們所說的插樁一般指字節(jié)碼插樁跪帝。
圖1是Android開發(fā)者常見的一張圖宝剖,我們編寫的源碼(.java)通過javac編譯成字節(jié)碼(.class),然后通過dx/d8編譯成dex文件歉甚。我們下面要講的插樁万细,就是在.class轉(zhuǎn)為.dex之前,修改.class文件從而達(dá)到修改或替換代碼的目的纸泄。
使用場(chǎng)景
①代碼插入
假如我們需要監(jiān)控項(xiàng)目中所有的方法耗時(shí)赖钞。如果手動(dòng)添加,一方面工作十分耗時(shí)聘裁,除了浪費(fèi)了自己的時(shí)間雪营,簡直沒有任何價(jià)值。另一方面做這么重復(fù)無意義的工作衡便,根本就體現(xiàn)不出自己的價(jià)值献起。而通過字節(jié)碼插樁,我們掃描每一個(gè)生成的.class文件镣陕,并針對(duì)特定規(guī)則進(jìn)行字節(jié)碼修改從而達(dá)到監(jiān)控每個(gè)方法耗時(shí)的目的谴餐。
②代碼替換
加入我們需要將 項(xiàng)目中的Dialog.show()方法替換成自己包裝的方法MyDialog.show(),如果用快捷鍵呆抑,可能會(huì)有問題【本人遇到過這種問題】:
- 如果其他類定義了show方法岂嗓,并被調(diào)用了,使用快捷鍵可能會(huì)錯(cuò)誤替換掉
這里我們也可以通過插樁來解決鹊碍。很多業(yè)務(wù)場(chǎng)景都可以使用插樁技術(shù)厌殉,比如無痕埋點(diǎn)、性能監(jiān)控侈咕」保【性能監(jiān)控劃重點(diǎn),性能優(yōu)化方面又多了一個(gè)有力的手段耀销!】
實(shí)現(xiàn)原理
android編譯原理就是將我們寫的java或者kotlin文件經(jīng)過javac或者kotlinc編譯期編譯完成后楼眷,統(tǒng)一變成.class字節(jié)碼文件,class字節(jié)碼隨后會(huì)被編譯成dex文件,并且和資源等文件打包性能最后的apk文件摩桶。
字節(jié)碼插樁就是在.class轉(zhuǎn)成.dex之前去修改class文件桥状,從而達(dá)到修改或者替換代碼的功能帽揪。關(guān)鍵在于如何獲取到.class轉(zhuǎn)成.dex的時(shí)間點(diǎn)以及相關(guān)的.class文件硝清!
android提供了一個(gè)Transform的接口,我們只需要實(shí)現(xiàn)一個(gè)gradle插件转晰,并且在插件里面提供一個(gè)自定義的Transform芦拿,注冊(cè)到構(gòu)建過程中。就可以在.class轉(zhuǎn)成.dex之前收到對(duì)應(yīng)的回調(diào)查邢,在方法的回調(diào)里就可以拿到已經(jīng)編譯好的蔗崎,全部的.class文件集合,接下來就可以對(duì)其進(jìn)行修改扰藕。class文件是字節(jié)碼文件缓苛,各種二進(jìn)制,手動(dòng)解析會(huì)很麻煩邓深,我們可以借助ASM工具解析未桥,修改,甚至是生成新的class文件芥备。通過這個(gè)工具就可以不用關(guān)心復(fù)雜的字節(jié)碼序列冬耿,將工作重心放在我們的字節(jié)碼插樁上。
功能梳理
為什么路由框架中要用字節(jié)碼插樁技術(shù)呢萌壳?
之前我們通過注解處理器生成了路由表亦镶,這個(gè)路由表是一個(gè)module中所有路由映射的表單。但是一個(gè)稍微有點(diǎn)規(guī)模的項(xiàng)目都會(huì)有好多module袱瓮,對(duì)于日活百萬缤骨、千萬、億級(jí)的app來說尺借,其module更可能多達(dá)上百個(gè)荷憋。在運(yùn)行時(shí),為了能打開所有的頁面褐望,必不可少的要找到所有頁面的注冊(cè)信息勒庄,也就是這眾多module中的路由表。人工查找必不可少會(huì)導(dǎo)致遺漏瘫里。
換種思路实蔽,無論是我們子工程中的路由表信息,還是打包成arr/jar包中的路由信息谨读,都會(huì)被編譯成.class文件局装,并達(dá)成.dex文件。所以我們可以拿到.class轉(zhuǎn)成.dex的時(shí)間點(diǎn),獲取所有的.class文件铐尚,解析字節(jié)碼文件拨脉。找到我們每一個(gè)路由表類,把這些類匯總起來宣增,形成一個(gè)固定名稱的映射表玫膀。這樣在后續(xù)運(yùn)行中,我們只需要注冊(cè)這一個(gè)匯總表就不會(huì)產(chǎn)生遺漏的問題了爹脾。
實(shí)戰(zhàn):創(chuàng)建類結(jié)構(gòu)
我們要做的第一步就是【收集固定包名帖旨、且前綴是RouterMapping】的所有路由表信息,通過ASM遍歷拿到所有的信息灵妨。然后在Transform中生成新的類:匯總表解阅。要實(shí)現(xiàn)這些,我們先定一個(gè)三步走戰(zhàn)略:
①建立Transform
②收集目標(biāo)類
③生成匯總映射表
1.建立Transform
Transform詳解
Transform+ASM
Transform API
transform任務(wù)在app/build/intermediates/transform/
下可以看到泌霍。
①引入依賴货抄,實(shí)現(xiàn)Transform
Transform是gradle里的類,在使用此類之前朱转,我們需要在我們的buildSrc下的build.gradle文件中引入gradle構(gòu)建工具:
接著在buildSr目錄下新建一個(gè)文件:RouterMappingTransform.groovy蟹地;
實(shí)現(xiàn)Transform抽象類;覆寫其中的抽象方法:
package com.freedom.gradle
import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
class RouterMappingTransform extends Transform {
/**
* 當(dāng)前 Transform 的名稱
* @return
*/
@Override
String getName() {
return "RouterMappingTransform"
}
/**
* 返回告知編譯器肋拔,當(dāng)前Transform需要消費(fèi)的輸入類型
* 在這里是CLASS類型
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 告知編譯器锈津,當(dāng)前Transform需要收集的范圍
* @return
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
/**
* 是否支持增量
* 通常返回False
* @return
*/
@Override
boolean isIncremental() {
return false
}
/**
* 所有的class收集好以后,會(huì)被打包傳入此方法
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
void transform(TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
// 1. 遍歷所有的Input
// 2. 對(duì)Input進(jìn)行二次處理
// 3. 將Input拷貝到目標(biāo)目錄
RouterMappingCollector collector = new RouterMappingCollector()
// 遍歷所有的輸入
transformInvocation.inputs.each {
// 把 文件夾 類型的輸入凉蜂,拷貝到目標(biāo)目錄
it.directoryInputs.each { directoryInput ->
def destDir = transformInvocation.outputProvider
.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY)
collector.collect(directoryInput.file)
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)
collector.collectFromJarFile(jarInput.file)
FileUtils.copyFile(jarInput.file, dest)
}
}
println("${getName()} all mapping class name = "
+ collector.mappingClassName)
File mappingJarFile = transformInvocation.outputProvider.
getContentLocation(
"router_mapping",
getOutputTypes(),
getScopes(),
Format.JAR)
println("${getName()} mappingJarFile = $mappingJarFile")
if (mappingJarFile.getParentFile().exists()) {
mappingJarFile.getParentFile().mkdirs()
}
if (mappingJarFile.exists()) {
mappingJarFile.delete()
}
// 將生成的字節(jié)碼,寫入本地文件
FileOutputStream fos = new FileOutputStream(mappingJarFile)
JarOutputStream jarOutputStream = new JarOutputStream(fos)
ZipEntry zipEntry =
new ZipEntry(RouterMappingByteCodeBuilder.CLASS_NAME + ".class")
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(
RouterMappingByteCodeBuilder.get(collector.mappingClassName))
jarOutputStream.closeEntry()
jarOutputStream.close()
fos.close()
}
}
上面實(shí)現(xiàn)的方法窿吩,鏈接文章以及注釋已經(jīng)標(biāo)的很明確了茎杂。主要是transform方法:
其中transform方法中的內(nèi)容,我們來一步一步分析纫雁。
當(dāng)項(xiàng)目中所有的.class文件打包后之后煌往,就會(huì)調(diào)用此方法。也就是在這個(gè)方法中轧邪,我們可以拿到項(xiàng)目中所有的.class文件9舨薄!
除了transform方法,我們后需需要一步一步完善,在實(shí)現(xiàn)了上面的幾個(gè)方法后考阱,我們就可以將transform注冊(cè)到項(xiàng)目中了舰讹。
②Transform注冊(cè)
/**
* 注冊(cè)Transform
* @param project
*/
private void registerTransform(Project project) {
//有則說明是com.android.application的子工程蛔垢,一般就是主工程鬼贱;其他模塊的是apply plugin: 'com.android.library'
if (project.plugins.hasPlugin(AppPlugin)) { //目前發(fā)現(xiàn)只能在主工程注冊(cè)
AppExtension baseExtension = project.extensions.getByType(AppExtension)
Transform transform = new RouterMappingTransform()
baseExtension.registerTransform(transform)
}
}
在我們上面的RouterPlugin中的apply方法中調(diào)用此方法响蓉,即可完成Transform的注冊(cè)烁设。這里有個(gè)坑點(diǎn)檬某,就是Transform只能在主工程中注冊(cè)撬腾。我原本是在一個(gè)library module中注冊(cè)的,但是會(huì)報(bào)錯(cuò)恢恼。所以上面getByType中只能傳入AppExtension或者BaseExtension民傻,傳入LibraryExtension就會(huì)報(bào)錯(cuò)。
那么對(duì)應(yīng)的厅瞎,我們就需要在app模塊下的build.gradle文件中調(diào)用:
apply plugin:'com.rye.router'
來完成transform的注冊(cè)饰潜〕踝梗【當(dāng)然其他模塊下依然可以引用此插件和簸,只是一定要主工程中注冊(cè)!】
這里的 com.rye.router
是我buildSrc插件指定的plugin id碟刺,要替換成自己的buildSrc的plugin id;
【驗(yàn)證是否注冊(cè)成功】:
執(zhí)行./gradlew clean
后锁保;
執(zhí)行./gradlew :app:assembleDebug
;
接著在app->build->intermediates
文件夾下半沽,如果看到:
就說明已經(jīng)注冊(cè)成功了爽柒。
③遍歷所有的class文件
transform(~)方法是Transform的核心方法,用來處理輸入者填。處理流程有一定格式:
就是從TransformInvocation 獲取到總的輸入后浩村,分別按照 class目錄 和 jar文件
集合的方式進(jìn)行遍歷處理。
整個(gè)Transform難點(diǎn)就就在于這個(gè)方法中的處理:
1.正確占哟、高效的進(jìn)行文件目錄心墅、jar 文件的解壓、class 文件 IO 流的處理榨乎,保證在這個(gè)過程中不丟失文件和錯(cuò)誤的寫入
2.高效的找到要插樁的結(jié)點(diǎn)怎燥,過濾掉無效的 class
3.支持增量編譯
那么接下來就可以開始我們實(shí)現(xiàn)transform的第一步,遍歷所有的class文件【包括文件夾下的文件蜜暑、jar包下的文件】:
// 1. 遍歷所有的Input
// 2. 對(duì)Input進(jìn)行二次處理
// 3. 將Input拷貝到目標(biāo)目錄
RouterMappingCollector collector = new RouterMappingCollector()
// 遍歷所有的輸入
transformInvocation.inputs.each {
// 把 文件夾 類型的輸入铐姚,拷貝到目標(biāo)目錄
it.directoryInputs.each { directoryInput ->
def destDir = transformInvocation.outputProvider
.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY)
collector.collect(directoryInput.file)
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)
collector.collectFromJarFile(jarInput.file)
FileUtils.copyFile(jarInput.file, dest)
}
}
對(duì)于RouterMappingCollector里的內(nèi)容肛捍,我們下面會(huì)有分析隐绵。先簡單分析這兩個(gè)遍歷。
inputs是傳過來的輸入流拙毫,有兩種格式:jar和目錄格式
outputProvider 獲取輸出目錄依许,將修改的文件復(fù)制到輸出目錄,必須執(zhí)行恬偷。所以這里面的兩個(gè)遍歷和copy固定是很固定的悍手。不一樣的地方就在于我們collector中帘睦。下面就介紹一下這個(gè)RouterMappingCollector.
2.收集目標(biāo)類
①完成映射表類名的收集
我們要生成總的匯總表,那么在此之前坦康,必須得拿到項(xiàng)目中所有module下的每一個(gè)路由表信息竣付。
所以在這里,我們?cè)赽uildSrc文件夾下新建一個(gè)groovy文件滞欠,用于過濾出所有的路由表對(duì)應(yīng)的.class文件:RouterMappingCollector.groovy
這個(gè)工具類要做的事情就以一件:過濾出所有的路由表文件古胆。
首先我們要知道,我們要過濾的除了文件夾下的類還有就是jar包下所有的類筛璧。
因?yàn)橛锌赡苡衅渌寮昧宋覀兊穆酚刹寮菀铮⒈淮虺闪薺ar包供外界使用。
還有就要我們要怎么過濾出這些路由表信息夭谤?
那我們就要回頭看一下我們的路由表都有哪些特征了棺牧。主要有三個(gè)特征:
(1)包名:在我們的注解處理器RouterProcessor中指定了路由表生成的目標(biāo)文件夾,我個(gè)人用的文件夾路徑是:
private static final String PACKAGE_NAME = "com/dawn/come/mapping"
(2)文件前綴:RouterMapping_
(3)文件后綴:因?yàn)樵趖ransform方法中過濾所有的路由表文件朗儒,所以能拿到的肯定都是.class文件颊乘。后綴也就是確定的.class了。
那么我們接下里要做的就是過濾所有的File,包括文件目錄和jar包中的文件醉锄。
找出符合這三個(gè)條件的文件乏悄,就是我們每一個(gè)module中的路由文件了~,那么這個(gè)實(shí)現(xiàn)類里的內(nèi)容也就應(yīng)運(yùn)而生了:
package com.freedom.gradle
import java.util.jar.JarEntry
import java.util.jar.JarFile
class RouterMappingCollector {
private static final String PACKAGE_NAME = "com/dawn/come/mapping"
private static final String FILE_NAME_PREFIX = "RouterMapping_"
private static final String FILE_NAME_SUFFIX = ".class"
private final Set<String> mappingClassNames = new HashSet<>()
Set<String> getMappingClassName() {
return mappingClassNames
}
/**
* 收集class文件或者class文件目錄中的映射表類
* @param classFile
*/
void collect(File classFile) {
if (classFile == null || !classFile.exists()) {
return
}
if (classFile.isFile()) {
if (classFile.absolutePath.contains(PACKAGE_NAME)
&& classFile.name.startsWith(FILE_NAME_PREFIX)
&& classFile.name.endsWith(FILE_NAME_SUFFIX)) {
String className = classFile.name.replace(FILE_NAME_SUFFIX, "")
mappingClassNames.add(className)
}
} else {
classFile.listFiles().each { file ->
collect(file)
}
}
}
/**
* 收集jar包中的映射表類
* @param jarFile
*/
void collectFromJarFile(File jarFile) {
Enumeration<JarEntry> enumeration = new JarFile(jarFile).entries()
if (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.name
if (entryName.contains(PACKAGE_NAME)
&& entryName.contains(FILE_NAME_PREFIX)
&& entryName.contains(FILE_NAME_SUFFIX)) {
String className = entryName.replace(PACKAGE_NAME, "")
.replace("/", "")
.replace(FILE_NAME_SUFFIX, "")
mappingClassNames.add(className)
}
}
}
}
接下來就可以回到我們上邊提到的transform方法了恳不,在兩個(gè)遍歷中分別收集到路由表信息檩小。可以通過println方法打印出所有的路由表文件名烟勋。
④模擬生成的匯總表
當(dāng)我們收集好所有子module下的路由表信息后规求。接下來我們要做的一件事,就是模擬我們要生成的匯總表神妹!
為什么要模擬這個(gè)這個(gè)類颓哮?主要是為了看生成后的匯總表用asm如何生成!
因?yàn)槲覀兊膮R總表生成時(shí)機(jī)是.class文件打包成.dex之前鸵荠,不可能插入源文件.java,.kt這種冕茅,只能插入字節(jié)碼文件!那么字節(jié)碼文件如何創(chuàng)建呢蛹找?如果手動(dòng)創(chuàng)創(chuàng)建姨伤,其成本還是十分高的,因?yàn)樽止?jié)碼文件的樣式是如下樣子的:
如果這樣一點(diǎn)點(diǎn)的根據(jù)我們要生成的匯總表去拆成各種匯編命令庸疾,再編寫生成字節(jié)碼文件乍楚,就很麻煩!所以我們可以借助一些字節(jié)碼操作工具届慈,常用的有:
ASM徒溪、javassit等忿偷。我們使用的是ASM框架。
ASM官網(wǎng)
ASM是一個(gè)通用的Java字節(jié)碼操作和分析框架臊泌。它可以直接以二進(jìn)制形式用于修改現(xiàn)有類或動(dòng)態(tài)生成類
在生成我們匯總表之前鲤桥,我們最好的方式就是先寫出要生成后的匯總表的java文件,通過一個(gè)插件來查看其asm格式的代碼文件渠概,然后根據(jù)這個(gè)文件內(nèi)容茶凳,抽離出我們需要的asm命令。
模擬創(chuàng)建好后的文件:
這里的RouterMapping_XX就代表著我們各個(gè)module下的路由表信息播揪;
RouterMapping就是我們匯總表生成后的樣式贮喧。
下面我們會(huì)說明為什么要先模擬創(chuàng)建好后的匯總表。
3.生成匯總映射表
創(chuàng)建RouterMappingByteCodeBuilder.groovy文件猪狈,調(diào)用ASM命令創(chuàng)建匯總表的.class文件箱沦。
首先指定要生成匯總表的類名
public static final String CLASS_NAME = "com/dawn/come/mapping/generated/RouterMapping"
創(chuàng)建class文件的流程大致如下:
// 1. 創(chuàng)建一個(gè)類
// 2. 創(chuàng)建構(gòu)造方法
// 3. 創(chuàng)建get方法
// (1)創(chuàng)建一個(gè)Map
// (2)塞入所有映射表的內(nèi)容
// (3)返回map
問題就來了,我們需要用ASM來創(chuàng)建我們的匯總表罪裹。ASM框架學(xué)習(xí)成本還是有的饱普,但是我們可以利用現(xiàn)有的插件偷一下懶运挫。下面就介紹兩款騷騷的插件:
ASM Bytecode Outline 状共、ASM Bytecode Viewer
前者是Intellij Idea里的插件、后者是Android Studio中的插件谁帕。
而且前者并不兼容Android Studio.但后者有點(diǎn)小問題峡继,有時(shí)候可能看不到字節(jié)碼文件...
下載好插件后restart一下。就可以看到看到as的右側(cè)多了一欄:
接下來執(zhí)行
./gradlew :app:assembleDebug
這樣項(xiàng)目build之后匈挖,就可以右鍵我們的類文件:
點(diǎn)擊ASM ByteCode Viewer選項(xiàng)碾牌,就可以看到該源文件對(duì)應(yīng)的字節(jié)碼文件及ASM類型的文件了:
上面我們模擬創(chuàng)建了匯總表生成后的文件,就是為了通過此插件查看ASMified里的內(nèi)容儡循。RouterMappingByteCodeBuilder.groovy文件中的邏輯舶吗,主要就是參照這個(gè)目錄下的內(nèi)容進(jìn)行編寫,編寫好后該文件內(nèi)容如下:
package com.freedom.gradle
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
class RouterMappingByteCodeBuilder implements Opcodes {
public static final String CLASS_NAME = "com/dawn/come/mapping/generated/RouterMapping"
//需要補(bǔ)習(xí)asm知識(shí)
static byte[] get(Set<String> allMappingNames) {
// 1. 創(chuàng)建一個(gè)類
// 2. 創(chuàng)建構(gòu)造方法
// 3. 創(chuàng)建get方法
// (1)創(chuàng)建一個(gè)Map
// (2)塞入所有映射表的內(nèi)容
// (3)返回map
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)
//---------------創(chuàng)建類
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER,
CLASS_NAME,
null,
"java/lang/Object",
null)
//--------------創(chuàng)建構(gòu)造方法
// 生成或者編輯方法
MethodVisitor mv
// 創(chuàng)建構(gòu)造方法
mv = cw.visitMethod(Opcodes.ACC_PUBLIC,
"<init>",
"()V",
null,
null)
//開啟字節(jié)碼的訪問或編輯
mv.visitCode()
mv.visitVarInsn(Opcodes.ALOAD, 0)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL,
"java/lang/Object", "<init>", "()V", false)
mv.visitInsn(Opcodes.RETURN)
mv.visitMaxs(1, 1)
mv.visitEnd()
//---------------創(chuàng)建get方法
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
"get",
"()Ljava/util/Map;",
"()Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;",
null)
mv.visitCode()
mv.visitTypeInsn(NEW, "java/util/HashMap")
mv.visitInsn(DUP)
//得到HashMap的實(shí)例
mv.visitMethodInsn(INVOKESPECIAL,
"java/util/HashMap",
"<init>",
"()V",
false)
//保存實(shí)例
mv.visitVarInsn(ASTORE, 0)
// 向Map中择膝,逐個(gè)塞入所有映射表的內(nèi)容
allMappingNames.each {
mv.visitVarInsn(ALOAD, 0)
mv.visitMethodInsn(INVOKESTATIC,//!!!!!!!!!!!!!!!!!!!!采坑點(diǎn)誓琼,不要搞成SPECIAL
"com/dawn/come/mapping/$it",
"get", "()Ljava/util/Map;", false)
mv.visitMethodInsn(INVOKEINTERFACE,
"java/util/Map",
"putAll",
"(Ljava/util/Map;)V", true)//map是一個(gè)接口,要傳入true
}
// 返回map
mv.visitVarInsn(ALOAD, 0)
mv.visitInsn(ARETURN)
mv.visitMaxs(2, 2)
mv.visitEnd()
return cw.toByteArray()
}
}
到這一步肴捉,生成匯總表字節(jié)碼的邏輯就已經(jīng)完成了腹侣,接下來就是將這個(gè)字節(jié)碼寫到本地創(chuàng)建我們的匯總表文件。
將匯總表字節(jié)碼文件寫入到本地jar包中去
回到RouterMappingTransform.groovy文件的transform方法中去:
之前遍歷了所有的輸入文件并進(jìn)行了拷貝齿穗。在此之后傲隶,就是將匯總表寫入本地jar包中去:
File mappingJarFile = transformInvocation.outputProvider.
getContentLocation(
"router_mapping",
getOutputTypes(),
getScopes(),
Format.JAR)
println("${getName()} mappingJarFile = $mappingJarFile")
if (mappingJarFile.getParentFile().exists()) {
mappingJarFile.getParentFile().mkdirs()
}
if (mappingJarFile.exists()) {
mappingJarFile.delete()
}
// 將生成的字節(jié)碼,寫入本地文件
FileOutputStream fos = new FileOutputStream(mappingJarFile)
JarOutputStream jarOutputStream = new JarOutputStream(fos)
ZipEntry zipEntry =
new ZipEntry(RouterMappingByteCodeBuilder.CLASS_NAME + ".class")
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(
RouterMappingByteCodeBuilder.get(collector.mappingClassName))
jarOutputStream.closeEntry()
jarOutputStream.close()
fos.close()
其中println("${getName()} mappingJarFile = $mappingJarFile")
輸出了我們匯總表的位置窃页。
接著就是繼續(xù)
./gradlew clean
./gradlew :app:assembleDebug
看一下輸出:
可以看到我們的匯總表生成在90.jar包中跺株,進(jìn)入到這個(gè)路徑下:
調(diào)用unzip命令复濒,解壓當(dāng)前文件,看一下解壓好后的文件:
可以看到我們的匯總表已經(jīng)正確生成了!
第七章 運(yùn)行時(shí)處理
本章梳理:
建立runtime工程
新建一個(gè)android library工程乒省,名為router_runtime芝薇;app模塊引入此router_runtime依賴