使用Kotlin打造Android路由框架-KRouter

KRouter(https://github.com/richardwrq/KRouter)路由框架借助gradle插件、kapt實(shí)現(xiàn)了依賴注入、為Android平臺頁面啟動提供路由功能。
源碼不復(fù)雜,在關(guān)鍵地方也有注釋說明,建議打算或正在使用kapt+kotlinpoet遇到坑的同學(xué)可以fork一下項(xiàng)目,或許能找到你想要的答案,只要將整個流程了解清楚了,相信你自己也能擼一個輪子出來,目前許多開源框架daggerbutter knife、greendao等實(shí)現(xiàn)原理都是一致的。

從startActivity開始說起

在組件化開發(fā)的實(shí)踐過程中,當(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ù)對屬性進(jìn)行賦值時的代碼也比較繁瑣,于是在網(wǎng)上查找相關(guān)資料了解到目前主流的路由框架(ARouter、Router等)都支持這些功能,秉著盡量不重復(fù)造輪子的觀念我fork了ARouter項(xiàng)目,但是閱讀源碼后發(fā)現(xiàn)其暫時不支持Service的啟動,而我負(fù)責(zé)的項(xiàng)目里面全是運(yùn)行在后臺的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)用方式差不多?,F(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種處理方式:

  1. 若被注解的類是Activity的子類,那么最終的處理方式是startActivity;
  2. 若被注解的類是Service的子類,最終的處理方式有兩種,也就 是Android中啟動Service的兩種方式,使用哪種啟動方式取決于是否調(diào)用了withServiceConn函數(shù)添加了ServiceConnection;
  3. 若被注解的類是Fragment的子類,最終的處理方式是調(diào)用無參構(gòu)造函數(shù)構(gòu)造出這個類的實(shí)例,并調(diào)用setArguments(Bundle args)將請求參數(shù)傳入Fragment的bundle中,最后返回該實(shí)例

@Interceptor注解的類需實(shí)現(xiàn)IRouteInterceptor接口,這些類主要處理是否攔截路由的邏輯,比如某些需要登錄才能啟動的組件,就可以用到攔截器
@Inject用于標(biāo)記需要被注入的屬性
@Provider注解的類最終可以調(diào)用KRouter.getProvider(path: String)方法獲取該類的對象,如果該類實(shí)現(xiàn)了IProvider接口,那么init(context: Context)方法將被調(diào)用
這些注解最終都不會被編譯進(jìn)class文件中,在編譯時期這些注解會被收集起來最終交由不同的Annotation Processor去處理。

KRouter路由框架分為3個模塊:

  • KRouter-api模塊,作為SDK提供API供應(yīng)用調(diào)用,調(diào)用KRouter-compiler模塊生成的類中的方法加載路由表,處理路由請求
  • KRouter-compiler模塊,各種注解對應(yīng)的Processor的集合,編譯期運(yùn)行,負(fù)責(zé)收集路由組件,并生成kotlin代碼
  • KRouter-gradle-plugin模塊,自定義gradle插件,在項(xiàng)目構(gòu)建時期添加相關(guān)依賴以及相關(guān)參數(shù)的配置
各模塊運(yùn)行時期.png
KRouter-compiler

在介紹該模塊之前如果有同學(xué)不知道Annotation Processor的話建議先閱讀 Annotation Processing-Tool詳解, 一小時搞明白注解處理器(Annotation Processor Tool)這兩篇文章,簡單來說,APT就是javac提供的一個插件,它會搜集被指定注解所注解的元素(類、方法或者屬性),最終將搜集到的這些交給注解處理器Annotation Processor進(jìn)行處理,注解處理器通常會生成一些新的代碼(推薦大名鼎鼎的square團(tuán)隊(duì)造的輪子javapoet,這個開源庫提供了非常友好的API讓我們?nèi)ド蒍ava代碼),這些新生成的代碼會與源碼一起在同一個編譯時期進(jìn)行編譯。
但是Annotation Processorjavac提供的一個插件,也就是說它只認(rèn)識Java代碼,它壓根不知道kotlin是什么,所以如果是用kotlin編寫的代碼文件最終將會被javac給忽略,所幸的是JetBrains在2015年就推出了kapt來解決這一問題。而且既然有javapoet,那square那么牛逼的團(tuán)隊(duì)肯定也會造一個生成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)部其實(shí)就是一個個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會生成一個實(shí)現(xiàn)了IRouteLoader接口的類,在loadInto方法中把注解中的元數(shù)據(jù)與被注解的元素的部分信息存到RouteMetadata對象,然后將注解的路徑path作為key,RouteMetadata對象作為value保存在一個map當(dāng)中。生成的代碼如下(項(xià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()獲取該類實(shí)例,然后將其強(qiáng)制轉(zhuǎn)換為IRouteLoader類型,接著調(diào)用loadInto方法傳入map即可,現(xiàn)在問題來了,加載一個類我們需要知道這個類的路徑和名稱:com.x.y.ClassA,但是SDK并不知道KRouter-compiler會生成哪些類。
為此我準(zhǔn)備了兩種解決方案:

  1. 類似ARouter的做法,掃描所有dex文件,找出實(shí)現(xiàn)了ARouter接口的類,然后將這些類的ClassName緩存至本地,下次應(yīng)用啟動時如果存在緩存且沒有新增文件則讀取緩存內(nèi)容即可;
  2. 第二種是生成的類及其路徑遵循一定的規(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,在程序運(yùn)行的時候,我們的SDK只需要獲取項(xiàng)目中所有Module的名稱,然后依次加載它們并執(zhí)行loadInto方法即可。

基于性能考慮我采取了第二種方案,這就需要解決一個問題,因?yàn)?strong>RouteProcessor是無法知道當(dāng)前是處于哪個Module的,所以我們需要在Module的build.gradle做如下配置:

kapt {
    arguments {
        arg("moduleName", project.getName())
    }
}

這樣我們就配置了一個名為“moduleName”的參數(shù),它的值就是當(dāng)前Module的名稱。這個參數(shù)可以在ProcessingEnvironmentgetOptions()方法獲取的map中取出,
RouteInterceptor、Provider三者的處理流程大致相同,就不一一贅述了。
在這里提一下關(guān)于依賴注入Inject的實(shí)現(xiàn),關(guān)于如何對屬性進(jìn)行注入我想了兩種解決方案:

  1. 第一種就是通過反射,了解反射的同學(xué)都知道可以通過反射獲取類的運(yùn)行時注解,并且通過反射API為類的屬性進(jìn)行賦值,但由于時反射,所以性能上有所損耗,但是可以無視屬性的訪問權(quán)限;
  2. 第二種是生成需要被注入的類的擴(kuò)展方法,在擴(kuò)展方法里面對接收者的屬性進(jìn)行賦值,性能更好,但是缺點(diǎn)是無法對private以及protected成員進(jìn)行賦值。

一開始是希望偷懶,就選擇了第一種方案,但是問題來了,我知道Java的反射會有一些性能上的問題,但速度還不至于讓用戶感知明顯,但是當(dāng)我調(diào)用kotlin反射相關(guān)API時(最主要是獲取Properties相關(guān)API),發(fā)現(xiàn)第一次調(diào)用花費(fèi)的在4~5s 左右,之后調(diào)用速度是毫秒級的,我猜測是第一次調(diào)用加載了大量數(shù)據(jù),然后將這些數(shù)據(jù)緩存起來了,但這4~5s的調(diào)用時間實(shí)在是惡心,所以最終還是決定采用方案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]!")
    }
}

生成的類路徑與擴(kuò)展方法接收者的類路徑相同(解決Java包內(nèi)訪問權(quán)限問題),類名命名規(guī)則為擴(kuò)展方法接收者類路徑的“.“替換為”_“作為前綴,后綴為”_KRouter_Injector“,比如被被注入的類是com.github.richardwrq.krouter.activity.Main2Activity,那么自動生成的類為com.github.richardwrq.krouter.activity.com_github_richardwrq_krouter_activity_Main2Activity_KRouter_Injector

KRouter-api

該模塊其實(shí)就是提供API給用戶調(diào)用的SDK
上面提到SDK需要執(zhí)行KRouter-compiler模塊類的代碼才能真正完成路由表初始化的工作,由于最終編譯器會將所有Module打包成一個apk,所以在APP運(yùn)行時是不存在Module的概念的,但是按照解決方案2各Module生成的類會以Module名稱作為后綴,因此必須想辦法讓SDK獲取到項(xiàng)目中所有Module的名稱,考慮再三,我采取的解決方案是從assets目錄入手,在項(xiàng)目構(gòu)建時期創(chuàng)建一個task,這個task會在Module的src/main/assets目錄下生成一個“KRouter_ModuleName”的文件,在SDK初始化的時候只需要列出assets目錄下所有"KRouter_"開頭的文件并截取下劃線“_”后的內(nèi)容,即可得到一個包含所有Module名稱的列表。
下面給出SDK的類圖,同學(xué)們可以對照源碼參考

KRouter-api類圖.png

KRouter-gradle-plugin

完成上述兩個模塊后其實(shí)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)項(xiàng)目中Module較多時,手動在每一個Module加入這些配置未免有些蠢。。所以我寫了一個gradle插件用來自動完成這些配置工作,具體實(shí)現(xiàn)參考源碼,邏輯非常簡單,最后使用引用方式變成下面這樣:
在項(xiàng)目根目錄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)用多進(jìn)程環(huán)境下的頁面路
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,716評論 25 709
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,502評論 19 139
  • 聽到閨蜜放年假要來重慶耍我欣喜若狂,和丹青有一年多沒有見,她在深圳,我在重慶,賞的是同一輪明月,但相隔千里。...
    云影M閱讀 271評論 0 1
  • The sun has disappeared into a distance. 太陽慢慢消失在遠(yuǎn)方, There...
    譚樹君閱讀 527評論 0 1
  • 能為人師和好為人師的區(qū)別很大。
    十年一井閱讀 149評論 0 0

友情鏈接更多精彩內(nèi)容