ARouter 原理深度剖析:从 APT 到 ASM,彻底搞懂路由框架

ARouter 原理深度剖析:从 APT 到 ASM,彻底搞懂路由框架

一句话总结:ARouter = APT 编译期生成路由表 + DexFile/assets/ASM 三种方式加载 + Intent 标准跳转

前言

ARouter 是阿里巴巴开源的路由框架,核心解决 Android 组件化/模块化下的页面跳转、服务调用、拦截器等问题。本文从源码层面,把 ARouter 的整个工作原理彻底讲透。


一、整体架构

ARouter 的工作流程分为三个阶段:

复制代码
编译期(APT) → 构建期(Gradle Plugin + ASM) → 运行期(navigation)
阶段 干什么
编译期 APT 扫描 @Route、@Autowired 注解,生成路由表 Java 文件
构建期 Gradle Plugin 用 ASM 修改字节码,消除反射(可选)
运行期 查 Warehouse 路由表 → 拦截器链 → Intent 跳转

二、编译期:APT 生成路由表

ARouter 利用 APT(Annotation Processing Tool)在编译时扫描所有加了 @Route 注解的类,自动生成三类 Java 文件。

2.1 生成路由表 ------ IRouteGroup

java 复制代码
@Route(path = "/user/detail")
public class UserDetailActivity extends Activity { ... }

APT 会生成:

java 复制代码
package com.alibaba.android.arouter.routes;

public class ARouter$$Group$$user implements IRouteGroup {

    @Override
    public void loadInto(Map<String, RouteMeta> atlas) {
        atlas.put("/user/detail",
            RouteMeta.build(
                RouteType.ACTIVITY,
                UserDetailActivity.class,
                "/user/detail",
                "user"    // 分组:路径第一段
            )
        );
        atlas.put("/user/profile",
            RouteMeta.build(
                RouteType.ACTIVITY,
                UserProfileActivity.class,
                "/user/profile",
                "user"
            )
        );
    }
}

分组规则 :路径第一段就是组名,/user/detailuser 组,/pay/checkoutpay 组。

2.2 生成根路由表 ------ IRouteRoot(存分组索引)

java 复制代码
package com.alibaba.android.arouter.routes;

public class ARouter$$Root$$moduleapp implements IRouteRoot {

    @Override
    public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
        routes.put("user", ARouter$$Group$$user.class);
        routes.put("pay",  ARouter$$Group$$pay.class);
        routes.put("main", ARouter$$Group$$main.class);
    }
}

这个就是分组索引表------只存"组名 → 组加载类"的映射。

2.3 生成参数注入 ------ Autowired

java 复制代码
public class ARouter$$Autowired$$UserDetailActivity implements ISyringe {

    @Override
    public void inject(Object target) {
        UserDetailActivity activity = (UserDetailActivity) target;
        activity.id   = activity.getIntent().getStringExtra("id");
        activity.name = activity.getIntent().getStringExtra("name");
        activity.age  = activity.getIntent().getIntExtra("age", 0);
    }
}

@Autowired 注解在编译期就生成了对应的参数注入代码,避免运行时用反射解析 Intent Extra

2.4 生成目录结构

ruby 复制代码
app/build/generated/source/kapt/debug/
└── com/alibaba/android/arouter/routes/
    ├── ARouter$$Root$$app.java            ← 根路由表(分组索引)
    ├── ARouter$$Root$$module_user.java
    ├── ARouter$$Group$$main.java          ← 路由表
    ├── ARouter$$Group$$user.java
    ├── ARouter$$Group$$pay.java
    ├── ARouter$$Interceptors$$app.java    ← 拦截器注册
    ├── ARouter$$Providers$$app.java       ← 服务提供者
    ├── ARouter$$Autowired$$UserDetailActivity.java  ← 参数注入
    ├── ARouter$$Autowired$$LoginActivity.java
    └── ARouter$$Autowired$$PayActivity.java

每个 module 各自生成 ,打包时合并在同一个包 com.alibaba.android.arouter.routes 下。


三、初始化加载阶段(核心演进之路)

这是 ARouter 最有意思的部分------加载方案经历了三次演进

3.1 方案一:DexFile 全量扫描(最原始的版本)

java 复制代码
public synchronized static void init(Context context, ThreadPoolExecutor tpe) {
    String apkPath = context.getPackageManager()
        .getApplicationInfo(context.getPackageName(), 0).sourceDir;

    DexFile dexFile = new DexFile(apkPath);
    Enumeration<String> entries = dexFile.entries();

    while (entries.hasMoreElements()) {
        String className = entries.nextElement();

        // 只过滤 routes 包下的类
        if (className.startsWith("com.alibaba.android.arouter.routes")) {
            // 反射实例化,调用 loadInto() 填充路由表
            Object instance = Class.forName(className).newInstance();

            if (instance instanceof IRouteRoot) {
                ((IRouteRoot) instance).loadInto(Warehouse.groupsIndex);
            }
        }
    }
}

原理 :运行时通过 DexFile 遍历 APK 中所有类名 ,做 startsWith 匹配,找到 routes 包下的生成类,反射创建实例后调用 loadInto()

问题

问题 影响
遍历全量类 项目大了 APK 有几万个类,每次都要遍历
首次初始化慢 MultiDex + 大项目可能 200ms+
低端机更明显 字符串匹配 + 反射,低端机上更卡

3.2 方案二:APT 阶段写入文本文件(中间过渡方案)

在 APT 阶段,把所有生成的类全限定名写到一个 text 文件中,打包到 APK 的 assets/arouter/ 目录下:

ruby 复制代码
assets/arouter/class_cache.txt

com.alibaba.android.arouter.routes.ARouter$$Root$$app
com.alibaba.android.arouter.routes.ARouter$$Root$$module_user
com.alibaba.android.arouter.routes.ARouter$$Group$$main
com.alibaba.android.arouter.routes.ARouter$$Group$$user
com.alibaba.android.arouter.routes.ARouter$$Interceptors$$app
com.alibaba.android.arouter.routes.ARouter$$Providers$$app
...

初始化时直接读文件,不需要遍历全量类:

java 复制代码
InputStream is = context.getAssets().open("arouter/class_cache.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String className;
while ((className = reader.readLine()) != null) {
    if (className.startsWith(ROUTE_ROOT_PAKCAGE)) {
        Object instance = Class.forName(className).newInstance();
        // ...
    }
}

优势:O(1) 定位到需要的类,初始化速度大幅提升。

注意:这不是 SPI(ServiceLoader),就是纯 APT 生成 + 自己读写文本文件。

3.3 方案三:Gradle Plugin + ASM 修改字节码(终极方案)

方案二虽然快了很多,但反射创建对象 的开销仍然存在。终极方案是:在字节码层面 直接改写 LogisticsCenter.init() 方法,把 loadInto() 调用硬编码进去。

ASM 原理

kotlin 复制代码
1. APT 生成 IRouteGroup 等 Java 文件
2. javac 编译成 .class 文件
3. Gradle Plugin 拦截 class → dex 的过程
   → 用 ASM 读取 LogisticsCenter.class
   → 在 init() 方法里直接插入调用代码
   → 输出修改后的 .class 文件

ASM 插入前的 init():

java 复制代码
public void init() {
    // 空的,或者只有基础逻辑
}

ASM 插入后,相当于字节码上改成了:

java 复制代码
public void init() {
    new ARouter$$Root$$app().loadInto(Warehouse.groupsIndex);
    new ARouter$$Root$$module_user().loadInto(Warehouse.groupsIndex);
    new ARouter$$Group$$user().loadInto(Warehouse.routes);
    new ARouter$$Group$$pay().loadInto(Warehouse.routes);
    new ARouter$$Interceptors$$app().loadInto(Warehouse.interceptors);
    new ARouter$$Providers$$app().loadInto(Warehouse.providers);
    // 原始 init 逻辑
}

注意 :这里是直接 new + 方法调用,没有反射没有 Class.forName()

3.4 三种方案对比

DexFile APT + 写文件 ASM Plugin
加载方式 运行时遍历所有类 读 assets 清单 编译期直接改字节码
反射 ✅ Class.forName + newInstance ✅ Class.forName + newInstance ❌ 直接 new
初始化耗时 最慢(几十~几百ms) 中等 最快(接近 0)
侵入性 需要 gradle plugin
代码维护 代码被魔改,难 debug

3.5 三合一兜底策略

最新版本 ARouter 是 三合一兜底

csharp 复制代码
init() 时:
  1. 尝试读 assets/arouter/class_cache.txt → 走反射(方案二)
  2. 如果 ASM Plugin 被应用 → LogisticsCenter.class 已被魔改 → 直接 new(方案三)
  3. 上述都不行 → 回退到 DexFile 扫描(方案一)

四、运行时导航流程

4.1 调用入口

java 复制代码
ARouter.getInstance()
    .build("/user/detail")
    .withString("id", "123")
    .navigation(this);

4.2 build() → 构造 Postcard(路由卡片)

java 复制代码
protected Postcard build(String path) {
    // 校验 path 格式
    if (TextUtils.isEmpty(path) || !path.startsWith("/")) {
        throw new HandlerException("...");
    }
    // 提取分组:/user/detail → group = "user"
    String group = path.substring(1, path.indexOf("/", 1));
    // 构造空壳 Postcard
    return new Postcard(path, group);
}

Postcard 结构

java 复制代码
public final class Postcard {
    private String path;            // "/user/detail"
    private String group;           // "user"
    private String action;          // Intent Action
    private Uri uri;                // 外部 URI
    private Bundle extras;          // 参数集
    private int flags;              // Intent flags
    private int enterAnim, exitAnim; // 转场动画
    private Object tag;             // 自定义附加数据
    private boolean greenChannel;   // 是否跳过拦截器
}

4.3 withString() → 塞参数

就是往 Postcard 的 Bundle 里放数据,每次返回 this,链式调用。

这是最核心的部分。 整个跳转就是跟 Warehouse 里的几个 Map 打交道。

4.4.1 Warehouse(全局数据仓库)
java 复制代码
final class Warehouse {
    // ① 路由主表:path → RouteMeta(含目标 class)
    static Map<String, RouteMeta> routes = new ConcurrentHashMap<>();

    // ② 分组索引:group → IRouteGroup.class(用完即删)
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new ConcurrentHashMap<>();

    // ③ 拦截器索引
    static Map<Class<?>, Integer> interceptorsIndex = new IdentityHashMap<>();

    // ④ 拦截器实例(已排序)
    static List<IInterceptor> interceptors = new ArrayList<>();

    // ⑤ Provider 缓存(已创建的服务实例)
    static Map<Class, IProvider> providers = new ConcurrentHashMap<>();

    // ⑥ Provider 索引
    static Map<String, RouteMeta> providersIndex = new ConcurrentHashMap<>();
}

Warehouse = 全局唯一的内存数据中心,所有东西最终都存在这里。

4.4.2 导航核心流程
vbnet 复制代码
navigation("/user/detail")
    │
    ▼
① routes.get("/user/detail")    ← 先查路由主表
    │
    ├── 命中 → 直接拿到 Class<?> → 跳转到 ③
    │
    └── 未命中 →
            │
            ▼
        ② groupsIndex.get("user")   ← 查分组索引
            │
            ├── 未命中 → 抛异常 "No route match"
            │
            └── 命中 → ARouter$$Group$$user.class
                    │
                    ▼
                反射 new ARouter$$Group$$user()
                .loadInto(Warehouse.routes)   ← 加载整个 user 组
                Warehouse.groupsIndex.remove("user")  ← 用完即删
                    │
                    ▼
                再查 routes.get("/user/detail") → 必然命中
    │
    ▼
③ 拿到 RouteMeta.destination = UserDetailActivity.class
    │
    ▼
  构建 Intent → startActivity

为啥要分组+懒加载?

ruby 复制代码
Warehouse 初始状态:
  groupsIndex = {
    "user" → ARouter$$Group$$user.class,
    "main" → ARouter$$Group$$main.class,
    "pay"  → ARouter$$Group$$pay.class
  }
  routes = {}  ← 空的!

第一次跳 /user/detail:
  → routes.get("/user/detail") = null
  → groupsIndex.get("user") = ARouter$$Group$$user.class
  → 加载 user 组所有路由到 routes
  → routes.get("/user/detail") → 命中!
  → groupsIndex.remove("user")

第二次跳 /user/profile:
  → routes.get("/user/profile") → 命中!(上一步已加载)
  → 不需要再查 groupsIndex

按需分组加载------不是一次性加载所有路由,第一次跳到某个组才加载该组的路由表。

4.5 拦截器链

java 复制代码
// _ARouter.java
interceptorService.doInterceptions(postcard, new InterceptorCallback() {
    @Override
    public void onContinue(Postcard postcard) {
        _navigation(mContext, postcard, requestCode, callback);
    }

    @Override
    public void onInterrupt(Throwable exception) {
        callback.onLost(postcard);
    }
});

拦截器实现:

java 复制代码
@Interceptor(priority = 7)
public class LoginInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
        if (需要登录) {
            callback.onInterrupt(new RuntimeException("需要登录"));
        } else {
            callback.onContinue(postcard);
        }
    }
}

原理 :按 priority 排序后的拦截器列表,在子线程中链式执行,每个拦截器决定放行或拦截。

4.6 _navigation() → 真正跳转

java 复制代码
private void _navigation(Context context, Postcard postcard, int requestCode, ...) {
    switch (postcard.getType()) {
        case ACTIVITY:
            Intent intent = new Intent(context, postcard.getDestination());
            intent.putExtras(postcard.getExtras());
            if (requestCode > 0) {
                ((Activity) context).startActivityForResult(intent, requestCode);
            } else {
                context.startActivity(intent);
            }
            break;

        case PROVIDER:
            // 服务提供者 → 直接 new 实例
            Object instance = postcard.getDestination().newInstance();
            if (instance instanceof IProvider) {
                ((IProvider) instance).init(context);
            }
            break;

        case FRAGMENT:
            Fragment fragment = (Fragment) postcard.getDestination().newInstance();
            fragment.setArguments(postcard.getExtras());
            break;
    }
}

最终就是一发 Intent ,所有复杂的 APT、ASM、懒加载、拦截器链,最后落到底就是 startActivity(intent)


五、完整时序图

scss 复制代码
你的代码            ARouter              LogisticsCenter          Warehouse            Interceptors        Android Framework
  │                   │                       │                     │                      │                    │
  ├─ build("/user/detail")                     │                     │                      │                    │
  │  └─ 构造 Postcard ──→ │                       │                     │                      │                    │
  │                       │                       │                     │                      │                    │
  ├─ withString()  ────→  │                       │                     │                      │                    │
  │                       │                       │                     │                      │                    │
  ├─ navigation()         │                       │                     │                      │                    │
  │                       ├─ completion() ────→ │                     │                      │                    │
  │                       │                       ├─ routes.get(path)─→│                      │                    │
  │                       │                       │  ← null(首次加载)  │                      │                    │
  │                       │                       ├─ groupsIndex.get()→│                      │                    │
  │                       │                       │  ← Group.class     │                      │                    │
  │                       │                       ├─ loadInto() ──────→│ routes.put()         │                    │
  │                       │                       ├─ routes.get(path)─→│                      │                    │
  │                       │                       │  ← RouteMeta       │                      │                    │
  │                       │                       │                     │                      │                    │
  │                       ├─ 填 Postcard  ←───── │                     │                      │                    │
  │                       │                       │                     │                      │                    │
  │                       ├─ doInterceptions()    │                     ├─ interceptors ────→│                    │
  │                       │                       │                     │                      ├─ process() ──┐  │
  │                       │                       │                     │                      │    校验逻辑    │  │
  │                       │                       │                     │                      │ ← onContinue() │  │
  │                       │                       │                     │                      │                    │
  │                       ├─ _navigation()        │                     │                      │                    │
  │                       │  └─ Intent + startActivity ──────────────────────────────────────→│  ActivityManager  │
  │                       │                       │                     │                      │                    │
  │  ← 跳转完成                                   │                     │                      │                    │

六、ARouter 还做了哪些有意思的事

6.1 Service 暴露(模块间解耦)

不仅仅是 Activity 跳转,ARouter 还能暴露接口服务:

java 复制代码
// 定义接口
public interface HelloService extends IProvider {
    String sayHello(String name);
}

// 实现
@Route(path = "/service/hello")
public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String name) { return "Hello " + name; }
}

// 调用
HelloService service = (HelloService) ARouter.getInstance()
    .build("/service/hello").navigation();

原理和 Activity 路由一样,只不过 navigation() 返回的是实例。

6.2 SharedPreferences 缓存

ARouter 还用 SharedPreferences 做了快速短路:

java 复制代码
// 判断 APK 版本/签名指纹是否变化
// 没变 → 路由表还在内存里,跳过所有扫描
// 变了 → 重新扫描,更新 SP

6.3 参数注入(@Autowired)

java 复制代码
@Route(path = "/user/detail")
public class UserDetailActivity extends Activity {

    @Autowired
    String id;       // 自动从 Intent Extra 注入

    @Autowired(name = "nick")
    String name;     // 别名映射

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ARouter.getInstance().inject(this);  // 触发注入
    }
}
java 复制代码
package com.alibaba.android.arouter.routes;

public class ARouter$$Autowired$$UserDetailActivity
        implements ISyringe {

    @Override
    public void inject(Object target) {
        UserDetailActivity activity = (UserDetailActivity) target;
        activity.id = activity.getIntent()
            .getStringExtra("id");
        activity.name = activity.getIntent()
            .getStringExtra("name");
        activity.age = activity.getIntent()
            .getIntExtra("age", 0);
    }
}
java 复制代码
// _ARouter.java
public void inject(Object target) {
    // 1. 通过 target 的类名,找到编译期生成的 AutowiredService 实现类
    //    类名规则:target 类全名 + "$$ARouter$$Autowired"
    String className = target.getClass().getName();
    
    // 比如 target = UserDetailActivity
    // className = "com.example.UserDetailActivity$$ARouter$$Autowired"
    
    // 2. 从缓存取,没取到就反射创建
    ISyringe autowiredHelper = Warehouse.autowiredCache.get(className);
    
    if (null == autowiredHelper) {
        // 反射加载生成的类
        autowiredHelper = (ISyringe) Class.forName(className).newInstance();
      
        Warehouse.autowiredCache.put(className, autowiredHelper);
    }
    
    // 3. 调用注入逻辑
    autowiredHelper.inject(target);
}

其实就是根据类名找ARouter AutowiredAutowired AutowiredUserDetailActivity,然后调用inject 编译期生成对应注入代码,运行时直接赋值,无反射开销。


总结

理解 ARouter,记住三句话:

  1. 编译期用 APT 生成路由表、根路由表、Autowired 注入代码
  2. 加载方式经历 DexFile 全量扫 → assets 文件读 → ASM 改字节码 三次演进,核心目标是消除反射
  3. 运行时就是查 Warehouse 的 Map → 拦截器链 → 发 Intent,没什么黑魔法

好了,现在你对 ARouter 的理解可以超过 90% 的 Android 开发者了。

相关推荐
程序员清风1 小时前
AI不是万能的,大家要专注实践!
java·后端·面试
清泓y2 小时前
UE移动开发技术面试题
android·面试·ue5·ue4·游戏程序
刘沅2 小时前
Redis的哨兵机制
后端·面试
凉凉的知识库2 小时前
什么是 HTTP Keep-Alive?一文讲清连接复用机制
网络协议·http·面试
清泓y3 小时前
UE基础知识与引擎架构面试题
面试·架构·ue5·ue4·游戏程序
触底反弹3 小时前
💡 React 父子组件通信:一个进度条教会我的 5 件事
前端·react.js·面试
艾莉丝努力练剑5 小时前
【MYSQL】MYSQL学习的一大重点:基本查询(下)
android·数据库·学习·mysql·面试·八股文
测试界的世清13 小时前
各大厂软件测试面试题+答案纯干货
面试·职场和发展
liang_jy14 小时前
内存管理(七)—— 内存映射
面试·操作系统