路由框架实现:ARouter原理、使用KSP实现页面路由注解处理
路由框架,说白了就是帮你解决「页面跳转」这件事的。你可能会想:不就是startActivity吗?嗯,小项目确实够用。但一旦项目膨胀到几十上百个模块,你会发现手动管理跳转路径简直是一场噩梦。
我在几年前接手过一个大型电商项目,光业务模块就有二十多个。每次改个页面路径,都得全局搜索字符串,生怕漏掉哪个地方。更别提跨模块调用了——A模块想跳B模块的页面,还得先依赖B模块的类。耦合度高得吓人。后来我们引入了ARouter,才算是把这个问题彻底解决了。
ARouter的核心原理
ARouter是阿里开源的路由框架,它的核心思想其实很简单:用注解标记页面,编译时生成路由表,运行时通过路由表完成跳转。
具体来说,ARouter做了三件事:
- 注解标记:用@Route注解标记Activity或Fragment,指定路径
- 编译期处理:通过APT(Annotation Processing Tool)扫描注解,生成路由映射文件
- 运行时加载:App启动时加载路由表,提供跳转服务
你想想看,这其实就是个「注册中心」模式。每个页面在编译期就把自己注册到路由表中,运行时直接查表跳转,完全不需要硬编码类名。
核心流程:
@Route(path="/main/home") → APT处理 → 生成路由表文件 → 运行时加载 → 根据path找到目标Activity → 完成跳转
ARouter的APT处理细节
ARouter用的是Java的APT机制,在编译期扫描所有被@Route注解的类。它会生成一个以"ARouter$$Group$$"开头的类文件,里面维护了该分组下的所有路由映射关系。
举个例子,假设你有这样一个页面:
@Route(path = "/user/profile")
public class UserProfileActivity extends AppCompatActivity {
// ...
}
APT会生成类似这样的代码:
public class ARouter$$Group$$user implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/user/profile", RouteMeta.build(
RouteType.ACTIVITY,
UserProfileActivity.class,
"/user/profile",
"user",
null, -1, -2147483648
));
}
}
运行时,ARouter会通过类名反射加载这些生成类,把路由信息填充到一个全局Map中。之后每次调用ARouter.getInstance().build("/user/profile").navigation(),就是从Map里查找到目标Activity,然后执行startActivity。
我个人习惯:路由路径的命名最好遵循「模块/功能」的格式,比如"/user/profile"、"/order/detail"。这样一眼就能看出属于哪个模块,后期维护也方便。
为什么需要KSP?
ARouter用的是APT,但APT有个问题——它只能处理Java源文件,不能直接处理Kotlin源文件。虽然Kotlin可以通过kapt兼容APT,但kapt的性能一直是个痛点。KSP(Kotlin Symbol Processing)是Google专门为Kotlin设计的注解处理器,它直接操作Kotlin的语法树,速度比kapt快2-3倍。
我在项目中做过实测:一个包含200多个Kotlin文件的模块,kapt编译需要40秒,换成KSP后降到了15秒。这个差距在大型项目中会非常明显。
使用KSP实现页面路由注解处理
好,接下来我们手写一个简化版的路由框架,用KSP处理注解。这样你就能彻底理解背后的原理了。
第一步:定义注解
// 路由注解
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Route(
val path: String,
val group: String = ""
)
这里我加了个group字段。为什么要分组?因为路由表如果太大,一次性加载会影响启动速度。分组后可以按需加载,比如用户模块的页面只在用户进入相关功能时才加载路由表。
第二步:创建KSP处理器
class RouteSymbolProcessor : SymbolProcessor {
private lateinit var codeGenerator: CodeGenerator
private lateinit var logger: KSPLogger
override fun process(resolver: Resolver): List<KSAnnotated> {
// 1. 获取所有被@Route注解的类
val symbols = resolver.getSymbolsWithAnnotation("com.example.Route")
.filterIsInstance<KSClassDeclaration>()
// 2. 收集路由信息
val routeMap = mutableMapOf<String, MutableList<RouteInfo>>()
symbols.forEach { symbol ->
val annotation = symbol.annotations
.first { it.shortName.asString() == "Route" }
val path = annotation.arguments
.first { it.name?.asString() == "path" }
.value as String
val group = annotation.arguments
.firstOrNull { it.name?.asString() == "group" }
?.value as? String ?: extractGroupFromPath(path)
val className = symbol.qualifiedName?.asString() ?: return@forEach
routeMap.getOrPut(group) { mutableListOf() }
.add(RouteInfo(path, className))
}
// 3. 生成路由表文件
generateRouteFiles(routeMap)
return emptyList()
}
private fun generateRouteFiles(routeMap: Map<String, List<RouteInfo>>) {
routeMap.forEach { (group, routes) ->
val file = codeGenerator.createNewFile(
dependencies = Dependencies(false),
packageName = "com.example.router",
fileName = "RouteTable_${group}"
)
file.appendText("""
package com.example.router
object RouteTable_${group} {
fun load(): Map = mapOf(
${routes.joinToString(",\n") { "\"${it.path}\" to \"${it.className}\"" }}
)
}
""".trimIndent())
file.close()
}
}
}
这段代码的核心逻辑就三步:扫描注解、收集信息、生成文件。嗯,其实跟APT的思路一模一样,只是API换成了KSP的。
我曾经踩过一个坑:KSP的resolver.getSymbolsWithAnnotation()返回的是所有被注解的符号,包括接口、抽象类。如果你只处理Activity,一定要加类型过滤。否则生成的路由表里混入了抽象类,运行时反射创建实例会直接崩溃。
第三步:注册KSP处理器
在模块的build.gradle.kts中配置:
plugins {
id("com.google.devtools.ksp")
}
dependencies {
implementation(project(":router-annotation"))
ksp(project(":router-processor"))
}
然后在src/main/resources/META-INF/services目录下创建文件com.google.devtools.ksp.processing.SymbolProcessor,内容写上你的处理器全限定类名。
第四步:运行时加载路由表
object Router {
private val routeTable = mutableMapOf<String, String>()
fun init(context: Context) {
// 通过反射加载所有生成的路由表
val tables = listOf("RouteTable_user", "RouteTable_order", "RouteTable_main")
tables.forEach { tableName ->
try {
val clazz = Class.forName("com.example.router.$tableName")
val method = clazz.getMethod("load")
@Suppress("UNCHECKED_CAST")
val map = method.invoke(null) as Map<String, String>
routeTable.putAll(map)
} catch (e: Exception) {
// 模块不存在时跳过
}
}
}
fun navigate(path: String, context: Context) {
val className = routeTable[path]
?: throw IllegalArgumentException("路由未注册: $path")
val clazz = Class.forName(className)
val intent = Intent(context, clazz)
context.startActivity(intent)
}
}
你看,运行时逻辑其实很简单——就是查表、反射、跳转。真正的复杂度都在编译期处理完了。
KSP vs APT:性能对比
| 对比维度 | APT (kapt) | KSP |
|---|---|---|
| 处理对象 | Java源文件(Kotlin需先转Java存根) | 直接处理Kotlin语法树 |
| 编译速度 | 慢(需生成Java存根) | 快2-3倍 |
| 增量编译 | 支持较差 | 原生支持 |
| Kotlin兼容性 | 通过kapt桥接 | 原生支持 |
| API复杂度 | 较复杂(需处理Element、TypeMirror等) | 更简洁(直接操作KSNode) |
从表格能看出来,KSP在Kotlin项目里几乎是全面领先。我个人建议:新项目直接用KSP,老项目如果Kotlin代码占比超过60%,也值得迁移。
SVG:路由框架核心流程
实际项目中的避坑指南
讲完了原理和实现,我再分享几个实际项目中遇到的坑:
- 路由路径冲突:多人协作时,不同模块可能定义了相同的路径。我建议在CI流程中加一个路由表检查脚本,发现重复路径直接报错。
- 模块化后路由表加载:如果模块是动态加载的(比如插件化),路由表需要在模块加载时动态注册,不能只在App启动时加载一次。
- 参数传递:路由跳转通常需要传参。ARouter的做法是用Bundle,但更好的方案是定义接口,通过依赖注入传递参数。我在项目中就封装了一个
RouteParam注解,配合KSP自动生成参数解析代码。 - 拦截器:路由框架一般都会提供拦截器机制,用于登录校验、埋点等。实现方式是在路由表中加一个拦截器列表,跳转前依次执行。
一个小技巧:KSP处理注解时,可以用resolver.getNewFiles()获取增量编译时新增的文件,只处理变化的部分,能进一步提升编译速度。
好了,关于路由框架的原理和KSP实现,就讲到这里。你可能会觉得代码量不大,但核心思想就是「编译期生成路由表,运行时查表跳转」。把这个思路吃透了,不管是ARouter还是其他路由框架,你都能快速上手。
公众号:蓝海资料掘金营,微信deep3321