三方庫源碼筆記(4)-ARouter 本身實現一個

對於 Android Developer 來講,不少開源庫都是屬於開發必備的知識點,從使用方式到實現原理再到源碼解析,這些都須要咱們有必定程度的瞭解和運用能力。因此我打算來寫一系列關於開源庫源碼解析實戰演練的文章,初定的目標是 EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil 等七個知名開源庫,但願對你有所幫助 😇😇java

公衆號:字節數組git

系列文章導航:github

上一篇文章中對 ARouter 的源碼進行了一次全面解析,原理懂得了,那麼就也須要進行一次實戰才行。對於一個優秀的第三方庫,開發者除了要學會如何使用外,更有難度的用法就是去了解實現原理、懂得如何改造甚至本身實現。本文就來本身動手實現一個路由框架,本身實現的目的不在於作到和 ARouter 同樣功能完善,而只是一個練手項目,目的是在於加深對 ARouter 的原理理解,因此本身的自定義實現就叫 EasyRouter 吧 😂😂api

EasyRouter 支持同個模塊間及跨模塊實現 Activity 的跳轉,僅須要指定一個字符串 path 便可:數組

EasyRouter.navigation(EasyRouterPath.PATH_HOME)
複製代碼

最終實現的效果:markdown

EasyRouter 的實現及使用一共涉及如下幾個模塊:app

  1. app。即項目的主模塊,從這裏跳到子模塊
  2. base。用於在多個模塊間共享 path
  3. easyrouter-annotation。用於定義和 EasyRouter 實現相關的註解Bean 對象
  4. easyrouter-api。用於定義和 EasyRouter 實現相關的 API 入口
  5. easyrouter-processor。用於定義和 EasyRouter 實現相關的註解處理器,在編譯階段使用
  6. easyrouter_demo。子模塊,用於測試 app 模塊跳轉到子模塊是否正常

EasyRouter 的實現思路和 ARouter 略有不一樣。EasyArouter 將同個模塊下的全部路由信息經過靜態方法塊來進行存儲並初始化,最終會生成如下的輔助文件:框架

package github.leavesc.easyrouter;

import java.util.HashMap;
import java.util.Map;

import github.leavesc.ctrlcv.easyrouter.EasyRouterHomeActivity;
import github.leavesc.ctrlcv.easyrouter.EasyRouterSubPageActivity;
import github.leavesc.easyrouterannotation.RouterBean;

/** * 這是自動生成的代碼 by leavesC */
public class EasyRouterappLoader {
    public static final Map<String, RouterBean> routerMap = new HashMap<>();

    {
        routerMap.put("app/home", new RouterBean(EasyRouterHomeActivity.class, "app/home", "app"));
        routerMap.put("app/subPage", new RouterBean(EasyRouterSubPageActivity.class, "app/subPage", "app"));
    }
}
複製代碼

因爲靜態變量和靜態方法塊在類被加載前是不會被初始化的,因此也能夠作到按需加載。即只有在外部發起跳轉到 app 這個模塊的請求的時候,EasyRouter 纔會去實例化 EasyRouterappLoader 類,此時纔會去加載 app 模塊的全部路由表信息,從而避免了內存浪費ide

下面再來簡單介紹下 EasyRouter 的實現過程oop

1、前置準備

因爲路由框架是以模塊爲單位的,因此同個模塊內的路由信息均可以存到同一個輔助文件中,而爲了不多個模塊間出現生成的輔助文件重名的狀況,因此外部須要主動配置每一個模塊的特定惟一標識,而後在編譯階段經過 AbstractProcessor 拿到這個惟一標識

例如,我爲 easyrouter-test 這個模塊設置的惟一標識就是 RouterTest

kapt {
    arguments {
        arg("EASYROUTER_MODULE_NAME", "RouterTest")
    }
}
複製代碼

最終生成的輔助文件對應的包名會是固定的,但類名會包含這個惟一標識。而因爲包名和類名的生成規則是有規律的,也方便在運行時拿到這個類,同時這也就要求同個模塊下的路由路徑 path 必須是屬於同個 group

package github.leavesc.easyrouter;

import java.util.HashMap;
import java.util.Map;

import github.leavesc.easyrouter_test.EasyRouterTestAActivity;
import github.leavesc.easyrouterannotation.RouterBean;

/** * 這是自動生成的代碼 by leavesC */
public class EasyRouterRouterTestLoader {
    public static final Map<String, RouterBean> routerMap = new HashMap<>();

    {
        routerMap.put("RouterTest/testA", new RouterBean(EasyRouterTestAActivity.class, "RouterTest/testA", "RouterTest"));
    }
}
複製代碼

@Router 用於對 Activity 進行標註,僅須要設置一個參數 path 便可,path 包含的第一個單詞就是 group

/** * @Author: leavesC * @Date: 2020/10/6 1:08 * @Desc: * @Github:https://github.com/leavesC */
@MustBeDocumented
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
annotation class Router(val path: String)

data class RouterBean(val targetClass: Class<*>, val path: String, val group: String)
複製代碼

2、註解處理器

聲明一個 EasyRouterProcessor 類繼承於 AbstractProcessor,在編譯階段經過掃描代碼元素從而拿到 @Router 註解的信息

/** * @Author: leavesC * @Date: 2020/10/5 22:17 * @Desc: * @Github:https://github.com/leavesC */
class EasyRouterProcessor : AbstractProcessor() {

    companion object {

        private const val KEY_MODULE_NAME = "EASYROUTER_MODULE_NAME"

        private const val PACKAGE_NAME = "github.leavesc.easyrouter"

        private const val DOC = "這是自動生成的代碼 by leavesC"

    }

    private lateinit var elementUtils: Elements

    private lateinit var messager: Messager

    private lateinit var moduleName: String

    override fun init(processingEnvironment: ProcessingEnvironment) {
        super.init(processingEnvironment)
        elementUtils = processingEnv.elementUtils
        messager = processingEnv.messager
        val options = processingEnv.options
        moduleName = options[KEY_MODULE_NAME] ?: ""
        if (moduleName.isBlank()) {
            messager.printMessage(Diagnostic.Kind.ERROR, "$KEY_MODULE_NAME must not be null")
        }
    }

	···

    override fun getSupportedAnnotationTypes(): MutableSet<String> {
        return mutableSetOf(Router::class.java.canonicalName)
    }

    override fun getSupportedSourceVersion(): SourceVersion {
        return SourceVersion.RELEASE_8
    }

    override fun getSupportedOptions(): Set<String> {
        return hashSetOf(KEY_MODULE_NAME)
    }

}
複製代碼

首先須要生成的 routerMap這個用於存儲路由表信息的 Map 字段,其 key 值即 path,value 值即 path 對應的頁面信息

//生成 routerMap 這個靜態常量
    private fun generateSubscriberField(): FieldSpec {
        val subscriberIndex = ParameterizedTypeName.get(
            ClassName.get(Map::class.java),
            ClassName.get(String::class.java),
            ClassName.get(RouterBean::class.java)
        )
        return FieldSpec.builder(subscriberIndex, "routerMap")
            .addModifiers(
                Modifier.PUBLIC,
                Modifier.STATIC,
                Modifier.FINAL
            )
            .initializer("new ${"$"}T<>()", HashMap::class.java)
            .build()
    }
複製代碼

以後就須要生成靜態方法塊。拿到 @Router 註解包含的 path 屬性,及被註解的類對應的 Class 對象,以此來構建一個 RouterBean 對象並存到 routerMap

//生成靜態方法塊
    private fun generateInitializerBlock( elements: MutableSet<out Element>, builder: TypeSpec.Builder ) {
        val codeBuilder = CodeBlock.builder()
        elements.forEach {
            val router = it.getAnnotation(Router::class.java)
            val path = router.path
            val group = path.substring(0, path.indexOf("/"))
            codeBuilder.add(
                "routerMap.put(${"$"}S, new ${"$"}T(${"$"}T.class, ${"$"}S, ${"$"}S));",
                path,
                RouterBean::class.java,
                it.asType(),
                path,
                group
            )
        }
        builder.addInitializerBlock(
            codeBuilder.build()
        )
    }
複製代碼

而後在 process方法中完成輔助文件的生成

override fun process( mutableSet: MutableSet<out TypeElement>, roundEnvironment: RoundEnvironment ): Boolean {
        val elements: MutableSet<out Element> =
            roundEnvironment.getElementsAnnotatedWith(Router::class.java)
        if (elements.isNullOrEmpty()) {
            return true
        }
        val typeSpec = TypeSpec.classBuilder("EasyRouter" + moduleName + "Loader")
            .addModifiers(Modifier.PUBLIC)
            .addField(generateSubscriberField())
            .addJavadoc(DOC)
        generateInitializerBlock(elements, typeSpec)
        val javaFile = JavaFile.builder(PACKAGE_NAME, typeSpec.build())
            .build()
        try {
            javaFile.writeTo(processingEnv.filer)
        } catch (e: Throwable) {
            e.printStackTrace()
        }
        return true
    }
複製代碼

3、EasyRouter

EasyRouter 這個單例對象即最終提供給外部的調用入口,總代碼行數不到五十行。外部經過調用 navigation 方法並傳入目標頁面 path 來實現跳轉,經過 path 來判斷其所屬 group,並嘗試加載其所在模塊生成的輔助文件,若是加載成功則能成功跳轉,不然就 Toast 提示

/** * @Author: leavesC * @Date: 2020/10/5 23:45 * @Desc: * @Github:https://github.com/leavesC */
object EasyRouter {

    private const val PACKAGE_NAME = "github.leavesc.easyrouter"

    private lateinit var context: Application

    private val routerByGroupMap = hashMapOf<String, Map<String, RouterBean>>()

    fun init(application: Application) {
        this.context = application
    }

    fun navigation(path: String) {
        val routerBean = getRouterLoader(path)
        if (routerBean == null) {
            Toast.makeText(context, "找不到匹配的路徑:$path", Toast.LENGTH_SHORT).show()
            return
        }
        val intent = Intent(context, routerBean.targetClass)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        context.startActivity(intent)
    }

    private fun getRouterLoader(path: String): RouterBean? {
        val group = path.substring(0, path.indexOf("/"))
        val map = routerByGroupMap[group]
        if (map == null) {
            var routerMap: Map<String, RouterBean>? = null
            try {
                val classPath = PACKAGE_NAME + "." + "EasyRouter" + group + "Loader"
                val clazz = Class.forName(classPath)
                val instance = clazz.newInstance()
                val routerMapField = clazz.getDeclaredField("routerMap")
                routerMap =
                    (routerMapField.get(instance) as? Map<String, RouterBean>) ?: hashMapOf()
                routerByGroupMap[group] = routerMap
            } catch (e: Throwable) {
                e.printStackTrace()
            } finally {
                if (routerMap == null) {
                    routerByGroupMap[group] = hashMapOf()
                }
            }
        }
        return routerByGroupMap[group]?.get(path)
    }

}
複製代碼

4、GitHub

因爲只是爲了加深對 ARouter 的實現原理的理解,因此纔來嘗試實現 EasyRouter,也不打算實現得多麼功能齊全,但對於一些讀者來講我以爲仍是有參考價值的😂😂 這裏也提供上述代碼的 GitHub 連接:AndroidOpenSourceDemo

相關文章
相關標籤/搜索