偿还技术债(4)-ARouter自己实现一个?
国庆假期想着闲着也是闲着,就想着来深入了解下几个常用的开源库??,看下其实现原理和源码,进行总结并输出成文章。初定的目标是 EventBus、ARouter、LeakCanary、Glide、Coil、Retrofit、OkHttp 等几个。目前已经完成了部分,在之后的几天里会将文章陆续发布出来?? 原文地址:https://juejin.im/user/923245496518439/posts
上一篇文章中对 ARouter 的源码进行了一次全面解析,原理懂得了,那么就也需要进行一次实战才行。对于一个优秀的第三方库,开发者除了要学会如何使用外,更有难度的用法就是去了解实现原理、懂得如何改造甚至自己实现。本文就来自己动手实现一个路由框架,因为自己实现的目的不在于做到和 ARouter 一样功能完善,而只是一个练手项目,目的是在于加深对 ARouter 的原理理解,所以自己的自定义实现就叫做 EasyArouter 吧 ??
EasyArouter 支持同个模块间及跨模块实现 Activity 的跳转,仅需要指定一个字符串 path 即可:
EasyRouter.navigation(EasyRouterPath.PATH_HOME)
最终实现的效果:
EasyArouter 的实现及使用一共涉及以下几个模块:
- app。即项目的主模块,从这里跳到子模块
- base。用于在多个模块间共享 path
- easyrouter-annotation。用于定义和 EasyArouter 实现相关的注解和 Bean 对象
- easyrouter-api。用于定义和 EasyArouter 实现相关的 API 入口
- easyrouter-processor。用于定义和 EasyArouter 实现相关的注解处理器,在编译阶段使用
- easyrouter-test。子模块,用于测试 app 模块跳转到子模块是否正常
EasyArouter 的实现思路和 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 这个模块的请求的时候,EasyArouter 才会去实例化 EasyRouterappLoader
类,此时才会去加载 app 模块的所有路由表信息,从而避免了内存浪费
下面再来简单介绍下 EasyArouter 的实现过程
一、前置准备
由于路由框架是以模块为单位的,所以同个模块内的路由信息都可以存到同一个辅助文件中,而为了避免多个模块间出现生成的辅助文件重名的情况,所以外部需要主动配置每个模块的特定唯一标识,然后在编译阶段通过 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
/**
* 作者:leavesC
* 时间:2020/10/5 22:08
* 描述:
* 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)
二、注解处理器
声明一个 EasyRouterProcessor
类继承于 AbstractProcessor
,在编译阶段通过扫描代码元素从而拿到 @Router
注解的信息
/**
* 作者:leavesC
* 时间:2020/10/5 22:17
* 描述:
* 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
}
三、EasyRouter
EasyRouter
这个单例对象即最终提供给外部的调用入口,总代码行数不到五十行。外部通过调用 navigation
方法并传入目标页面 path 来实现跳转,通过 path 来判断其所属 group,并尝试加载其所在模块生成的辅助文件,如果加载成功则能成功跳转,否则就 Toast 提示
/**
* 作者:leavesC
* 时间:2020/10/5 23:45
* 描述:
* 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)
}
}
四、结尾
由于只是为了加深对 ARouter 的实现原理的理解,所以才来尝试实现 EasyArouter,也不打算实现得多么功能齐全,但对于一些读者来说我觉得还是有参考价值的,这里也提供上述代码的 GitHub 链接:Demo
- 模拟退火算法从原理到实战【基础篇】
- python接口自动化3-自动发帖(session)
- 平面上给定n条线段,找出一个点,使这个点到这n条线段的距离和最小。
- python接口自动化4-绕过验证码登录(cookie)
- 洛谷P1313 计算系数【快速幂+dp】
- python接口自动化5-Json数据处理
- Numpy教程第1部分 - 阵列简介(常用基础操作总结)
- Session和Cookies的基本原理
- 浅析Numpy.genfromtxt及File I/O讲解
- 损失函数详解
- 排查Java的内存问题
- 使用两种方法让 ASP.NET Core 实现遵循 HATEOAS 结构的 RESTful API
- 设计模式六大原则(5):迪米特法则
- Selenium2+python自动化61-Chrome浏览器(chromedriver)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- chrome 插件通信DEOM
- rust 模块组织结构
- PCIe例程理解(一)用户逻辑模块(接收)仿真分析
- 细品服务并发限流+Redis-cell的使用
- 在VCUTRD 2020.1 里设置HDMI-TX显示QT界面
- 稀疏数组 & 环形队列
- Spring+Mybatis+Atomikos实现分布式事务
- Android低功耗蓝牙总结
- 【云原生技术研究】 从bpftrace看如何利用eBPF实现内核追踪
- Qt多线程编程
- JavaScript中的时间与日期、正则表达式和Function类型
- JavaScript中的变量、作用域、内存问题和基本包装类型
- 【LeetCode之C#解法】 移动零、爬楼梯
- jQuery框架概述
- 久等了!Docker容器常用命令