组件化之后,feature-user 和 feature-order 互相看不见对方的类,startActivity(Intent(this, OrderActivity::class.java)) 这行代码直接编译不过。ARouter 用一个字符串路径替代了类引用,让两个互不依赖的模块也能跳转、传参、调用服务。本文从注解处理、路由表加载、跳转流程、拦截器和服务发现五个环节,拆解 ARouter 的实现原理,并补上工程里常见的坑。
组件化跳转的核心矛盾
ruby
:app
/ \
:feature-user :feature-order
\ /
:lib-common
两个 feature 模块平级,不允许互相依赖,否则会出现循环依赖、编译变慢、边界失控。但业务上「用户页跳订单页」是刚需。
可选方案:
| 方案 | 问题 |
|---|---|
| 隐式 Intent + action | 每个页面都要在 Manifest 声明 action,字符串散落各处,无法传复杂参数 |
反射 Class.forName |
类名硬编码,混淆后失效,没有编译期检查 |
| 接口下沉到 common | 每加一个页面都要改 common,common 越来越胖 |
| 路由框架 | 路径 → 目标类的映射在编译期生成,运行时查表 |
ARouter 选的是最后一种:编译期生成映射表,运行时按路径查找。
一、注解与 APT:路由表是怎么生成的
声明路由
kotlin
@Route(path = "/order/detail")
class OrderDetailActivity : AppCompatActivity() {
@Autowired(name = "orderId")
@JvmField var orderId: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ARouter.getInstance().inject(this)
}
}
路径必须至少两级,第一级是 group (这里是 order)。分组是后面按需加载的关键。
编译期生成的三类文件
arouter-compiler 是一个 AbstractProcessor,扫描 @Route、@Interceptor、@Autowired 后,在 com.alibaba.android.arouter.routes 包下为每个模块生成:
java
// 1. Root:记录本模块有哪些 group
public class ARouter$$Root$$featureorder implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("order", ARouter$$Group$$order.class);
}
}
// 2. Group:记录某个 group 下的所有路由
public class ARouter$$Group$$order implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/order/detail", RouteMeta.build(
RouteType.ACTIVITY, OrderDetailActivity.class,
"/order/detail", "order",
new HashMap<String, Integer>() {{ put("orderId", 8); }},
-1, -2147483648));
}
}
// 3. Autowired 注入器
public class OrderDetailActivity$$ARouter$$Autowired implements ISyringe {
@Override
public void inject(Object target) {
OrderDetailActivity t = (OrderDetailActivity) target;
t.orderId = t.getIntent().getExtras() == null
? t.orderId
: t.getIntent().getExtras().getString("orderId", t.orderId);
}
}
RouteMeta 里存的是目标 Class 对象本身,不是类名字符串,所以混淆不会让它失效。
模块名要通过编译参数传给 APT,否则多个模块生成同名的 Root 类会冲突:
kotlin
// feature-order/build.gradle.kts
kapt {
arguments { arg("AROUTER_MODULE_NAME", project.name) }
}
二、初始化:两级懒加载
Warehouse:内存中的路由仓库
java
class Warehouse {
// group 名 → Group 类(初始化时填充)
static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
// 路径 → RouteMeta(按需填充)
static Map<String, RouteMeta> routes = new HashMap<>();
// 服务、拦截器相关缓存
static Map<Class, IProvider> providers = new HashMap<>();
static Map<String, RouteMeta> providersIndex = new HashMap<>();
static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("...");
static List<IInterceptor> interceptors = new ArrayList<>();
}
init 做了什么
ARouter.init(application) 调用 LogisticsCenter.init():
- 找出所有
com.alibaba.android.arouter.routes包下的类名 - 实例化所有
ARouter$$Root$$xxx,调用loadInto(Warehouse.groupsIndex) - 同样加载 Interceptor 和 Provider 的索引
注意:这一步只加载 group 索引,不加载具体路由。一个有 300 个页面、20 个 group 的 App,初始化时只往 Map 里放 20 条记录。某个 group 的路由要等到第一次跳转该 group 时才加载------这就是两级懒加载。
类扫描的性能问题
第 1 步默认实现是 ClassUtils.getFileNameByPackageName():打开 APK 里所有 dex 文件,遍历全部类名找前缀匹配。冷启动时这一步在低端机上能吃掉几百毫秒。
ARouter 有缓存:首次扫描结果存进 SharedPreferences,后续按 versionName + versionCode 判断是否复用。所以开发期每次都慢,正式包只有首次安装或升级后慢。
更彻底的方案是官方 Gradle 插件 com.alibaba.arouter。它在 Transform 阶段扫描所有 class,直接把注册代码插进 LogisticsCenter.loadRouterMap():
java
// 插件注入后的字节码等价于
private static void loadRouterMap() {
registerByPlugin = true;
register("com.alibaba.android.arouter.routes.ARouter$$Root$$featureorder");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$featureuser");
// ...
}
运行时不再扫 dex,初始化变成几次 Class.forName。
原版插件依赖 AGP 的 Transform API,AGP 8 已经移除。新项目需要使用社区维护的、基于
AsmClassVisitorFactory的适配插件,接入前确认它支持你的 AGP 版本。
三、一次跳转的完整流程
kotlin
ARouter.getInstance()
.build("/order/detail")
.withString("orderId", "A1024")
.navigation(context)
1. build:构造 Postcard
build(path) 从路径解析出 group(取第一段),返回 Postcard。Postcard 继承自 RouteMeta,外加 Bundle、flags、动画、超时等运行时信息。它就是一张「待寄出的明信片」。
如果注册了 PathReplaceService,这一步会先改写路径,可以用来做页面降级或 A/B 路由。
2. completion:查表补全
java
public synchronized static void completion(Postcard postcard) {
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (routeMeta == null) {
// 路由没加载过:找到对应 group 并加载整个 group
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
if (groupMeta == null) {
throw new NoRouteFoundException("There is no route match the path ...");
}
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
iGroupInstance.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(postcard.getGroup()); // 加载完就移除索引
completion(postcard); // 重新查一次
} else {
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
// ... 如果是 PROVIDER 类型,在这里实例化并缓存
}
}
两个细节:
- 加载一个 group 后把它从
groupsIndex删掉,防止重复加载 - 方法加了
synchronized,多线程同时首次访问同一 group 时不会重复执行loadInto
3. 拦截器链
如果 Postcard 不是 greenChannel(),交给 InterceptorService 走拦截器链(下一节展开)。
4. _navigation:真正执行
根据 RouteType 分发:
java
switch (postcard.getType()) {
case ACTIVITY:
Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
if (!(currentContext instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
runInMainThread(() -> startActivity(requestCode, currentContext, intent, postcard, callback));
break;
case PROVIDER:
return postcard.getProvider();
case FRAGMENT:
Object instance = postcard.getDestination().getConstructor().newInstance();
// 把 Bundle 设置为 arguments 后返回
return instance;
}
可以看到 ARouter 不仅能跳 Activity,还能拿到 Fragment 实例和服务实例。
四、拦截器:AOP 式的跳转控制
定义拦截器
kotlin
@Interceptor(priority = 1, name = "登录拦截")
class LoginInterceptor : IInterceptor {
override fun init(context: Context) {}
override fun process(postcard: Postcard, callback: InterceptorCallback) {
val needLogin = postcard.extra and FLAG_NEED_LOGIN != 0
if (needLogin && !UserManager.isLoggedIn()) {
callback.onInterrupt(IllegalStateException("未登录"))
} else {
callback.onContinue(postcard)
}
}
}
// 目标页通过 extras 打标记
@Route(path = "/order/detail", extras = FLAG_NEED_LOGIN)
class OrderDetailActivity : AppCompatActivity()
priority 越小越先执行,同一 priority 不允许重复(UniqueKeyTreeMap 会直接抛异常)。
拦截器链如何执行
java
// InterceptorServiceImpl
public void doInterceptions(final Postcard postcard, final InterceptorCallback callback) {
LogisticsCenter.executor.execute(() -> {
CancelableCountDownLatch latch = new CancelableCountDownLatch(Warehouse.interceptors.size());
_execute(0, latch, postcard);
latch.await(postcard.getTimeout(), TimeUnit.SECONDS); // 默认 300 秒
if (latch.getCount() > 0) {
callback.onInterrupt(new HandlerException("The interceptor processing timed out."));
} else if (postcard.getTag() != null) {
callback.onInterrupt((Throwable) postcard.getTag());
} else {
callback.onContinue(postcard);
}
});
}
private static void _execute(final int index, final CancelableCountDownLatch latch, final Postcard postcard) {
if (index < Warehouse.interceptors.size()) {
IInterceptor interceptor = Warehouse.interceptors.get(index);
interceptor.process(postcard, new InterceptorCallback() {
public void onContinue(Postcard postcard) {
latch.countDown();
_execute(index + 1, latch, postcard); // 递归下一个
}
public void onInterrupt(Throwable exception) {
postcard.setTag(exception);
latch.cancel(); // 计数直接清零,立即唤醒 await
}
});
}
}
关键点:
- 拦截器跑在子线程(ARouter 自己的线程池),所以拦截器里可以做同步 IO,但不能直接操作 UI
- 用
CountDownLatch等待整条链完成,onInterrupt通过cancel()提前放行 - 如果某个拦截器既不调
onContinue也不调onInterrupt,跳转会挂起直到超时------这是线上「点了没反应」的常见原因
五、服务发现:跨模块调用接口
跳转只解决了页面问题。模块间还需要调用方法,比如订单模块需要拿当前用户信息。
kotlin
// lib-common:只放接口
interface UserService : IProvider {
fun currentUserId(): String?
}
// feature-user:实现
@Route(path = "/user/service")
class UserServiceImpl : UserService {
override fun init(context: Context) {}
override fun currentUserId() = UserManager.uid
}
// feature-order:使用,完全不依赖 feature-user
val userService = ARouter.getInstance().navigation(UserService::class.java)
val uid = userService?.currentUserId()
按类型查找时,ARouter 用的是 providersIndex:APT 在 ARouter$$Providers$$xxx 里记录了「接口全限定名 → RouteMeta」的映射。找到后实例化、调用 init()、缓存到 Warehouse.providers,所以 IProvider 默认是单例。
注意 navigation(Class) 在找不到实现时返回 null,调用方要处理空值,否则独立运行某个 feature 模块时会直接崩溃。
工程实践中的常见问题
1. 路由找不到,但注解明明写了
按顺序排查:
- 该模块是否配置了
AROUTER_MODULE_NAME? - 该模块的
kapt/annotationProcessor是否引入了arouter-compiler? - 开发期是否命中了旧的路由缓存?调试时调用
ARouter.openDebug(),会在每次启动重新扫描 - 是否把
com.alibaba.android.arouter.routes.**混淆掉了?
proguard
-keep public class com.alibaba.android.arouter.routes.**{*;}
-keep public class com.alibaba.android.arouter.facade.**{*;}
-keep class * implements com.alibaba.android.arouter.facade.template.ISyringe{*;}
-keep interface * implements com.alibaba.android.arouter.facade.template.IProvider
2. 同一 group 分散在多个模块
kotlin
// feature-user
@Route(path = "/common/feedback") class FeedbackActivity
// feature-order
@Route(path = "/common/invoice") class InvoiceActivity
两个模块都会生成 ARouter$$Group$$common,同名类冲突,打包失败或其中一方被覆盖。约定:一个 group 只属于一个模块,最简单的做法是 group 名等于模块名。
3. Autowired 注入不生效
- Kotlin 字段必须加
@JvmField,否则 APT 生成的代码访问不到字段 - 忘记在
onCreate里调用ARouter.getInstance().inject(this) - 传递自定义对象时,需要实现
SerializationService,否则withObject()无法序列化
4. 迁移到 KSP
arouter-compiler 只支持 kapt/APT。项目切到 KSP 后要么保留 kapt,要么使用社区的 KSP 版本处理器。两者生成的类名和格式需要一致,迁移时先对比 build/generated 下的产物。
设计取舍
把 ARouter 的设计拆开看,每一步都是明确的取舍:
| 环节 | 做法 | 换来的 | 付出的 |
|---|---|---|---|
| 路由注册 | APT 生成代码 | 无反射查找,编译期发现重复路径 | 每个模块都要配置处理器 |
| 路由加载 | Root → Group 两级懒加载 | 初始化开销与页面数量解耦 | group 不能跨模块 |
| 类发现 | 扫 dex 或插件注入 | 不需要手写注册代码 | 默认方案启动慢 |
| 拦截器 | 子线程 + CountDownLatch | 拦截器里可以做异步/同步校验 | 漏调回调就会挂起 |
| 服务 | IProvider 单例缓存 | 跨模块调用像本地调用 | 生命周期跟随进程 |
理解了这些,就能判断自己的项目到底需要 ARouter 的哪一部分。如果只是单模块 App,Navigation Component 足够;如果是几十个模块、多团队并行开发,路由表 + 拦截器 + 服务发现这一整套才真正体现价值。