KRouter(https://github.com/richardwrq/KRouter)路由框架借助gradle插件站楚、kapt實現(xiàn)了依賴注入、為Android平臺頁面啟動提供路由功能乘综。
源碼不復(fù)雜,在關(guān)鍵地方也有注釋說明贸弥,建議打算或正在使用kapt+kotlinpoet遇到坑的同學(xué)可以fork一下項目禁荒,或許能找到你想要的答案,只要將整個流程了解清楚了尔崔,相信你自己也能擼一個輪子出來答毫,目前許多開源框架dagger
、butter knife
您旁、greendao
等實現(xiàn)原理都是一致的烙常。
從startActivity開始說起
在組件化開發(fā)的實踐過程中,當(dāng)我完成一個模塊的開發(fā)后(比如說這個模塊中有一個Activity或者Service供調(diào)用者調(diào)用)鹤盒,其他模塊的開發(fā)者要啟動我這個模塊中的Activity的代碼我們再熟悉不過了:
val intent = Intent(this, MainActivity::class.java)
intent.putExtra("param1", "1")
intent.putExtra("param2", "2")
startActivity(intent)
當(dāng)然,其他模塊的開發(fā)人員需要知道我們這個Activity的類名以及傳入的參數(shù)對應(yīng)的key值(上面的param1和param2)侦副,這時候我就想侦锯,在每一個需要啟動這個頁面的地方都存在著類似的樣板代碼,而且被啟動的Activity在取出參數(shù)對屬性進行賦值時的代碼也比較繁瑣秦驯,于是在網(wǎng)上查找相關(guān)資料了解到目前主流的路由框架(ARouter尺碰、Router等)都支持這些功能,秉著盡量不重復(fù)造輪子的觀念我fork了ARouter項目译隘,但是閱讀源碼后發(fā)現(xiàn)其暫時不支持Service的啟動亲桥,而我負(fù)責(zé)的項目里面全是運行在后臺的Service。固耘。题篷。
緊接著也大概了解了一下其他一些框架,都存在一些不太滿意的地方厅目,考慮再三番枚,干脆自己擼一個輪子出來好了法严。
首先來看一段最簡單的發(fā)起路由請求的代碼(Java調(diào)用):
KRouter.INSTANCE.create("krouter/main/activity?test=32")
.withFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.withString("test2", "this is test2")
.request();
其中krouter/main/activity?test=32
為對應(yīng)的路由路徑,可以使用類似http請求的格式葫笼,在問號后緊接著的是請求參數(shù)深啤,這些參數(shù)最終會自動包裝在intent的extras中,也可以通過調(diào)用with
開頭的函數(shù)來配置請求參數(shù)路星。
上面的代碼執(zhí)行后最終會啟動一個Activity溯街,準(zhǔn)確來說是一個帶有@Route
注解的Activity,它長這樣:
@Route(path = "krouter/main/activity")
public class MainActivity extends Activity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getIntent().getIntExtra("test", -1);//這里可以獲取到請求參數(shù)test
}
...
}
這是一個最基本的功能洋丐,怎么樣呈昔,看起來還不錯吧?跟大部分路由框架的調(diào)用方式差不多〉姘ぃ現(xiàn)在主流的路由框架是怎么做到的呢韩肝?下面就看我一一道來。
在使用KRouter
的API前首先需要為一些類添加注解:
/**
* User: WuRuiqiang(263454190@qq.com)
* Date: 18/1/2
* Time: 上午10:53
* Version: v1.0
* Description:用于標(biāo)記可路由的組件
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Route(
/**
* Path of route
*/
val path: String,
/**
* PathPrefix of route
*/
val pathPrefix: String = "",
/**
* PathPattern of route
*/
val pathPattern: String = "",
/**
* Name of route
*/
val name: String = "undefined",
/**
* Priority of route
*/
val priority: Int = -1)
/**
* User: WuRuiqiang(263454190@qq.com)
* Date: 18/1/2
* Time: 上午10:53
* Version: v1.0
* Description:用于攔截路由的攔截器
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Interceptor(
/**
* Priority of interceptor
*/
val priority: Int = -1,
/**
* Name of interceptor
*/
val name: String = "DefaultInterceptor")
/**
* User: WuRuiqiang(263454190@qq.com)
* Date: 18/1/2
* Time: 上午10:53
* Version: v1.0
* Description:屬性注入
*/
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class Inject(
/**
* Name of property
*/
val name: String = "",
/**
* If true, app will be throws NPE when value is null
*/
val isRequired: Boolean = false,
/**
* Description of the field
*/
val desc: String = "No desc.")
/**
* User: WuRuiqiang(263454190@qq.com)
* Date: 18/1/2
* Time: 上午10:53
* Version: v1.0
* Description:Provider
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Provider(/**
* Path of Provider
*/
val value: String)
被注解的元素的信息最終被保存在對應(yīng)的數(shù)據(jù)類中:
/**
* User: WuRuiqiang(263454190@qq.com)
* Date: 18/1/4
* Time: 上午10:46
* Version: v1.0
* Description:Route元數(shù)據(jù)九榔,用于存儲被[com.github.richardwrq.krouter.annotation.Route]注解的類的信息
*/
data class RouteMetadata(
/**
* Type of Route
*/
val routeType: RouteType = RouteType.UNKNOWN,
/**
* Priority of route
*/
val priority: Int = -1,
/**
* Name of route
*/
val name: String = "undefine",
/**
* Path of route
*/
val path: String = "",
/**
* PathPrefix of route
*/
val pathPrefix: String = "",
/**
* PathPattern of route
*/
val pathPattern: String = "",
/**
* Class of route
*/
val clazz: Class<*> = Any::class.java)
/**
* User: WuRuiqiang(263454190@qq.com)
* Date: 18/1/8
* Time: 下午10:46
* Version: v1.0
* Description:Interceptor元數(shù)據(jù)哀峻,用于存儲被[com.github.richardwrq.krouter.annotation.Interceptor]注解的類的信息
*/
data class InterceptorMetaData(
/**
* Priority of Interceptor
*/
val priority: Int = -1,
/**
* Name of Interceptor
*/
val name: String = "undefine",
/**
* Class desc of Interceptor
*/
val clazz: Class<*> = Any::class.java)
/**
* User: WuRuiqiang(263454190@qq.com)
* Date: 18/3/14
* Time: 上午1:28
* Version: v1.0
* Description:Injector元數(shù)據(jù),用于存儲被[com.github.richardwrq.krouter.annotation.Inject]注解的類的信息
*/
data class InjectorMetaData(
/**
* if true, throw NPE when the filed is null
*/
val isRequired: Boolean = false,
/**
* key
*/
val key: String = "",
/**
* field name
*/
val fieldName: String = "")
其中被@Route注解的類是Android中的四大組件和Fragment或者它們的子類(目前尚不支持Broadcast以及ContentProvider)哲泊,被@Route注解的對象目前有3種處理方式:
- 若被注解的類是Activity的子類剩蟀,那么最終的處理方式是startActivity;
- 若被注解的類是Service的子類切威,最終的處理方式有兩種育特,也就 是Android中啟動Service的兩種方式,使用哪種啟動方式取決于是否調(diào)用了
withServiceConn
函數(shù)添加了ServiceConnection先朦; - 若被注解的類是Fragment的子類缰冤,最終的處理方式是調(diào)用無參構(gòu)造函數(shù)構(gòu)造出這個類的實例,并調(diào)用
setArguments(Bundle args)
將請求參數(shù)傳入Fragment的bundle中喳魏,最后返回該實例
被@Interceptor注解的類需實現(xiàn)IRouteInterceptor接口棉浸,這些類主要處理是否攔截路由的邏輯,比如某些需要登錄才能啟動的組件刺彩,就可以用到攔截器
@Inject用于標(biāo)記需要被注入的屬性
被@Provider注解的類最終可以調(diào)用KRouter.getProvider(path: String)
方法獲取該類的對象迷郑,如果該類實現(xiàn)了IProvider接口,那么init(context: Context)
方法將被調(diào)用
這些注解最終都不會被編譯進class文件中创倔,在編譯時期這些注解會被收集起來最終交由不同的Annotation Processor去處理嗡害。
KRouter路由框架分為3個模塊:
- KRouter-api模塊,作為SDK提供API供應(yīng)用調(diào)用畦攘,調(diào)用KRouter-compiler模塊生成的類中的方法加載路由表霸妹,處理路由請求
- KRouter-compiler模塊,各種注解對應(yīng)的Processor的集合念搬,編譯期運行抑堡,負(fù)責(zé)收集路由組件摆出,并生成kotlin代碼
- KRouter-gradle-plugin模塊,自定義gradle插件首妖,在項目構(gòu)建時期添加相關(guān)依賴以及相關(guān)參數(shù)的配置
KRouter-compiler
在介紹該模塊之前如果有同學(xué)不知道Annotation Processor的話建議先閱讀 Annotation Processing-Tool詳解偎漫, 一小時搞明白注解處理器(Annotation Processor Tool)這兩篇文章,簡單來說有缆,APT就是javac
提供的一個插件象踊,它會搜集被指定注解所注解的元素(類、方法或者屬性)棚壁,最終將搜集到的這些交給注解處理器Annotation Processor進行處理杯矩,注解處理器通常會生成一些新的代碼(推薦大名鼎鼎的square團隊造的輪子javapoet,這個開源庫提供了非常友好的API讓我們?nèi)ド蒍ava代碼)袖外,這些新生成的代碼會與源碼一起在同一個編譯時期進行編譯史隆。
但是Annotation Processor是javac
提供的一個插件,也就是說它只認(rèn)識Java代碼曼验,它壓根不知道kotlin是什么泌射,所以如果是用kotlin編寫的代碼文件最終將會被javac
給忽略,所幸的是JetBrains在2015年就推出了kapt來解決這一問題鬓照。而且既然有javapoet熔酷,那square那么牛逼的團隊肯定也會造一個生成kotlin代碼的輪子吧,果不其然豺裆,在github一搜kotlinpoet拒秘,還真有,所以最終決定KRouter-compiler模塊使用kapt+kotlinpoet來自動生成代碼(kotlinpoet文檔過于簡單了臭猜,建議使用該庫的同學(xué)通過它的測試用例或者參照J(rèn)avapoet文檔了解API的調(diào)用)躺酒。
開頭的例子中我們可以看到使用KRouter啟動一個Activity只需要知道該Activity的路徑即可,并不需要像Android原生的啟動方式一樣傳入Class<*>
或者Class Name
蔑歌,那么KRouter是怎么做到的呢阴颖?
原理很簡單,KRouter-compiler模塊生了初始化路由表
的代碼丐膝,這些路由表
內(nèi)部其實就是一個個map,這些map以路徑path作為key钾菊,數(shù)據(jù)類作為value(比如RouteMetadata)帅矗,SDK內(nèi)部會通過path獲取到數(shù)據(jù)類,像開頭啟動Activity的例子中煞烫,SDK就通過path獲取到一個RouteMetadata對象浑此,在這個對象中取出被注解的類的Class<*>
,有了這個Class<*>
就可以完成啟動Activity的操作滞详。
接下來說說路由表
初始化代碼生成之后是怎么被執(zhí)行的凛俱,首先我定義了這樣一些接口:
/**
* 加載路由
*
* @author: Wuruiqiang <a href="mailto:263454190@qq.com">Contact me.</a>
* @version: v1.0
* @since: 18/1/4 下午6:38
*/
interface IRouteLoader {
fun loadInto(map: MutableMap<String, RouteMetadata>)
}
/**
* 加載攔截器
*
* @author: Wuruiqiang <a href="mailto:263454190@qq.com">Contact me.</a>
* @version: v1.0
* @since: 18/1/5 上午9:12
*/
interface IInterceptorLoader {
fun loadInto(map: TreeMap<Int, InterceptorMetaData>)
}
/**
* 加載Provider
*
* @author: Wuruiqiang <a href="mailto:263454190@qq.com">Contact me.</a>
* @version: v1.0
* @since: 18/1/5 上午9:12
*/
interface IProviderLoader {
fun loadInto(map: MutableMap<String, Class<*>>)
}
以@Route注解為例紊馏,在KRouter-compiler中定義了一個繼承自AbstractProcessor的類RouteProcessor,在編譯期間編譯器會收集@Route注解的元素的信息然后交由RouteProcessor處理蒲犬,RouteProcessor會生成一個實現(xiàn)了IRouteLoader接口的類朱监,在loadInto
方法中把注解中的元數(shù)據(jù)與被注解的元素的部分信息存到RouteMetadata對象,然后將注解的路徑path作為key原叮,RouteMetadata對象作為value保存在一個map當(dāng)中赫编。生成的代碼如下(項目build之后可以在(module)/build/generated/source/kaptKotlin/(buildType)
目錄下找到這些自動生成的類):
/**
* ***************************************************
* * THIS CODE IS GENERATED BY KRouter, DO NOT EDIT. *
* ***************************************************
*/
class KRouter_RouteLoader_app : IRouteLoader {
override fun loadInto(map: MutableMap<String, RouteMetadata>) {
map["krouter/sample/MainActivity"] = RouteMetadata(RouteType.ACTIVITY, -1, "undefined", "krouter/sample/MainActivity", "", "", MainActivity::class.java)
map["myfragment"] = RouteMetadata(RouteType.FRAGMENT_V4, -1, "undefined", "myfragment", "", "", MainActivity.MyFragment::class.java)
map["krouter/sample/fragment1"] = RouteMetadata(RouteType.FRAGMENT, -1, "undefined", "krouter/sample/fragment1", "", "", Fragment1::class.java)
map["krouter/sample/fragment2"] = RouteMetadata(RouteType.FRAGMENT, -1, "undefined", "krouter/sample/fragment2", "", "", Fragment2::class.java)
map["krouter/sample/Main2Activity"] = RouteMetadata(RouteType.ACTIVITY, -1, "undefined", "krouter/sample/Main2Activity", "", "", Main2Activity::class.java)
map["krouter/sample/Main3Activity"] = RouteMetadata(RouteType.ACTIVITY, -1, "undefined", "krouter/sample/Main3Activity", "", "", Main3Activity::class.java)
}
}
代碼生成之后,我們需要執(zhí)行loadInto
方法才算是把數(shù)據(jù)存入到map中去奋隶,我們可以通過Class.forName(ClassName).newInstance()
獲取該類實例擂送,然后將其強制轉(zhuǎn)換為IRouteLoader類型,接著調(diào)用loadInto
方法傳入map即可唯欣,現(xiàn)在問題來了嘹吨,加載一個類我們需要知道這個類的路徑和名稱:com.x.y.ClassA
,但是SDK并不知道KRouter-compiler會生成哪些類境氢。
為此我準(zhǔn)備了兩種解決方案:
- 類似
ARouter
的做法蟀拷,掃描所有dex文件,找出實現(xiàn)了ARouter
接口的類产还,然后將這些類的ClassName緩存至本地匹厘,下次應(yīng)用啟動時如果存在緩存且沒有新增文件則讀取緩存內(nèi)容即可; - 第二種是生成的類及其路徑遵循一定的規(guī)則,比如由RouteProcessor生成的類路徑規(guī)定為
com.github.richardwrq.krouter
阔逼,類名規(guī)定以“KRouter_RouteLoader_”作為開頭然后拼接上Module名稱(以Module名稱作為后綴是避免在不同的Module下生成類名一樣的類淹辞,導(dǎo)致編譯時出現(xiàn)類重復(fù)定義異常),所以RouteProcessor名稱為app
的Module下生成的類就是com.github.richardwrq.krouter.KRouter_RouteLoader_app
炕柔,在程序運行的時候,我們的SDK只需要獲取項目中所有Module的名稱媒佣,然后依次加載它們并執(zhí)行loadInto
方法即可匕累。
基于性能考慮我采取了第二種方案,這就需要解決一個問題默伍,因為RouteProcessor是無法知道當(dāng)前是處于哪個Module的欢嘿,所以我們需要在Module的build.gradle
做如下配置:
kapt {
arguments {
arg("moduleName", project.getName())
}
}
這樣我們就配置了一個名為“moduleName”的參數(shù),它的值就是當(dāng)前Module的名稱也糊。這個參數(shù)可以在ProcessingEnvironment的getOptions()
方法獲取的map中取出炼蹦,
Route、Interceptor狸剃、Provider三者的處理流程大致相同掐隐,就不一一贅述了。
在這里提一下關(guān)于依賴注入Inject的實現(xiàn)钞馁,關(guān)于如何對屬性進行注入我想了兩種解決方案:
- 第一種就是通過反射虑省,了解反射的同學(xué)都知道可以通過反射獲取類的運行時注解匿刮,并且通過反射API為類的屬性進行賦值,但由于時反射探颈,所以性能上有所損耗熟丸,但是可以無視屬性的訪問權(quán)限;
- 第二種是生成需要被注入的類的擴展方法膝擂,在擴展方法里面對接收者的屬性進行賦值虑啤,性能更好,但是缺點是無法對private以及protected成員進行賦值架馋。
一開始是希望偷懶狞山,就選擇了第一種方案,但是問題來了叉寂,我知道Java的反射會有一些性能上的問題萍启,但速度還不至于讓用戶感知明顯,但是當(dāng)我調(diào)用kotlin反射相關(guān)API時(最主要是獲取Properties相關(guān)API)屏鳍,發(fā)現(xiàn)第一次調(diào)用花費的在4~5s 左右勘纯,之后調(diào)用速度是毫秒級的,我猜測是第一次調(diào)用加載了大量數(shù)據(jù)钓瞭,然后將這些數(shù)據(jù)緩存起來了驳遵,但這4~5s的調(diào)用時間實在是惡心,所以最終還是決定采用方案2山涡,有興趣的同學(xué)可以查看com/github/richardwrq/krouter/compiler/processor/RouteProcessor.kt
堤结,生成的代碼如下:
class com_github_richardwrq_krouter_activity_Main2Activity_KRouter_Injector : IInjector {
override fun inject(any: Any, extras: Bundle?) {
val bundle = getBundle(any, extras)//getBundle為自動生成的頂層方法
(any as Main2Activity).exInject(bundle)
}
private fun Main2Activity.exInject(bundle: Bundle) {
person = bundle.get("person") as? Person ?: KRouter.getProvider<Person>("person") ?: parseObject(bundle.getString("person"), object : TypeToken<Person>() {}.getType()) ?: throw java.lang.NullPointerException("Field [person] must not be null in [Main2Activity]!")//parseObject為自動生成的頂層方法
provider = bundle.get("NoImplProvider") as? NoImplProvider ?: KRouter.getProvider<NoImplProvider>("NoImplProvider") ?: parseObject(bundle.getString("NoImplProvider"), object : TypeToken<NoImplProvider>() {}.getType()) ?: throw java.lang.NullPointerException("Field [provider] must not be null in [Main2Activity]!")
myProvider = (KRouter.getProvider<MyProvider>("provider/myprovider")) ?: throw java.lang.NullPointerException("Field [myProvider] must not be null in [Main2Activity]!")
}
}
生成的類路徑與擴展方法接收者的類路徑相同(解決Java包內(nèi)訪問權(quán)限問題),類名命名規(guī)則為擴展方法接收者類路徑的“.“替換為”_“作為前綴鸭丛,后綴為”_KRouter_Injector“竞穷,比如被被注入的類是com.github.richardwrq.krouter.activity.Main2Activity
,那么自動生成的類為com.github.richardwrq.krouter.activity.com_github_richardwrq_krouter_activity_Main2Activity_KRouter_Injector
KRouter-api
該模塊其實就是提供API給用戶調(diào)用的SDK
上面提到SDK需要執(zhí)行KRouter-compiler模塊類的代碼才能真正完成路由表初始化的工作鳞溉,由于最終編譯器會將所有Module打包成一個apk瘾带,所以在APP運行時是不存在Module的概念的,但是按照解決方案2各Module生成的類會以Module名稱作為后綴熟菲,因此必須想辦法讓SDK獲取到項目中所有Module的名稱看政,考慮再三,我采取的解決方案是從assets
目錄入手抄罕,在項目構(gòu)建時期創(chuàng)建一個task帽衙,這個task會在Module的src/main/assets
目錄下生成一個“KRouter_ModuleName”的文件,在SDK初始化的時候只需要列出assets
目錄下所有"KRouter_"開頭的文件并截取下劃線“_”后的內(nèi)容贞绵,即可得到一個包含所有Module名稱的列表。
下面給出SDK的類圖恍飘,同學(xué)們可以對照源碼參考
KRouter-gradle-plugin
完成上述兩個模塊后其實KRouter
框架已經(jīng)可以正常使用了榨崩,引用方式如下:
在各Module的build.gradle
加入下面的代碼
kapt {
arguments {
arg("moduleName", project.getName())
}
}
dependencies {
implementation 'com.github.richardwrq:krouter-api:x.y.z’
kapt 'com.github.richardwrq:krouter-compiler:x.y.z'
}
afterEvaluate {
//在assets目錄創(chuàng)建文件的task
...
}
當(dāng)項目中Module較多時谴垫,手動在每一個Module加入這些配置未免有些蠢。母蛛。所以我寫了一個gradle插件用來自動完成這些配置工作翩剪,具體實現(xiàn)參考源碼,邏輯非常簡單彩郊,最后使用引用方式變成下面這樣:
在項目根目錄build.gradle
文件加入如下配置
buildscript {
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:x.y.z"
classpath "com.github.richardwrq:krouter-gradle-plugin:x.y.z"
}
}
然后在各Module的build.gradle
文件加入如下配置
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: "com.github.richardwrq.krouter"
到這里KRouter
路由框架就粗略的介紹了一遍前弯,由于kapt
仍在不斷完善,所以使用過程中難免碰到一些坑或者本身API功能不夠完善秫逝,下面就列舉一些遇到的問題以及解決方法:
ToDoList
- 通過gradle插件修改
AndroidManifest.xml
文件恕出,自動注冊路由組件(Activity、Service) - 目前尚不支持動態(tài)加載的插件的路由注冊违帆,但有解決方案浙巫,hook classloader裝載方法,在加載dex文件時掃描
KRouter
的路由組件 - 支持多應(yīng)用多進程環(huán)境下的頁面路