[Android 从零到一] ARouter 路由框架原理:APT 生成路由表与组件化通信

组件化之后,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():

  1. 找出所有 com.alibaba.android.arouter.routes 包下的类名
  2. 实例化所有 ARouter$$Root$$xxx,调用 loadInto(Warehouse.groupsIndex)
  3. 同样加载 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. 路由找不到,但注解明明写了

按顺序排查:

  1. 该模块是否配置了 AROUTER_MODULE_NAME?
  2. 该模块的 kapt/annotationProcessor 是否引入了 arouter-compiler?
  3. 开发期是否命中了旧的路由缓存?调试时调用 ARouter.openDebug(),会在每次启动重新扫描
  4. 是否把 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 足够;如果是几十个模块、多团队并行开发,路由表 + 拦截器 + 服务发现这一整套才真正体现价值。

相关推荐
最好一梦1 小时前
佳维视安卓工控机在堆垛机中的应用
android
传奇开心果编程2 小时前
组合优先开发范式:从汉字造字原理到声明式UI组件组合思想
android·flutter·ios·swiftui·移动开发·harmonyos·android jetpack
腾讯端服务3 小时前
一码Duo端-Kuikly-iOS折叠屏新UI适配实践
android·ui·ios·kotlin·鸿蒙
T量子永生3 小时前
Android SISO 分布式编译集群配置方案
android·分布式·云计算·源代码管理
街头便衣3 小时前
OpenRouter周榜复算:164万亿token,输出2.55%,DeepSeek账单占1.4%
android·java·开发语言·人工智能·语言模型·开源
警醒与鞭策15 小时前
Codex桌面版与Claude桌面版使用设置
android·unity·ai·性能优化
传奇开心果18 小时前
【Compose Multiplatform跨端声明式UI实用技术】第19课:多平台部署与发布
android·kotlin
红尘伴蝶舞18 小时前
加密拦截器让抓包失效,我只能从 4KB 的 logcat 碎片里拼回 JSON
android·kotlin·ai编程
booboosui18 小时前
219 个还是 0 个:读不到微信界面时,我只加不删
android