终于懂了-ARouter原理初探

ARouter详解

ARouter是阿里开源的一个专门用于帮助Android App进行组件化改造的路由框架,其核心定位是解决组件化开发中的跨模块通信和解耦问题,通过路由机制实现模块间的页面跳转、服务调用和参数传递,彻底打破了传统开发中模块间的强依赖关系。它让不同模块的页面、服务能够在不直接依赖对方实现的情况下进行交互。本文主要讲ARouter的原理,适合学会ARouter基础使用还想继续探索深层原理的读者

核心设计思想

ARouter 的本质就是一个专为 Android 设计的路由框架,通过一个中央系统来管理和分发请求,从而实现不同模块(尤其是相互没有直接依赖关系的模块)之间的顺畅通信和页面跳转。

ARouter解决的问题本质就是模块之间如何做到不依赖实现的情况下在运行时动态定位页面/服务 ,基于该问题ARouter设计了编译时,初始化时,运行时的三层核心能力。

  1. 编译时:通过注解 Processor(注解处理器) 扫描注解(如 @Route),生成 Java 类,用于描述模块中的路由信息。这个过程就是典型的APT技术应用,其最大优势在于将耗时的类扫描和映射关系建立工作从App运行时转移到了编译时,避免了在运行时使用反射,从而大大提升了性能,这也是它比其他基于运行时扫描的路由性能更好、启动更快的原因。
  2. 初始化时:App启动时,ARouter会加载所有路由表类,构建一个内存中的路由索引。
  3. 运行时:根据根据路径创建一个路由信息卡片(PostCard),核心组件 LogisticsCenter会拿着这个路径到内存仓库 Warehouse中查找对应的 RouteMeta信息(包含目标Class等),并将其补全到Postcard中。最后,根据Postcard中的信息,通过Intent启动Activity或执行其他操作。

ARouter 之所以拆成编译、初始化、运行三步,是为了把"路径 → 类"的映射工作尽可能前置到编译期,让 App 在运行过程中不需要扫描 Dex、不需要反射查找目标类,从而实现几乎零成本跳转。 文章下面主要就围绕着三个时期进行讲解

预备知识

ARouter的源码前先讲几个比较核心的概念

PostCard

Postcard(明信片)是一个核心概念,它封装了一次路由跳转所需的全部信息,并负责驱动整个导航流程。

当我们调用 ARouter.getInstance().build("/app/main")时,就对应生成了一个PostCard对象。

它的的本质是一个"未完全解析的跳转请求",它把跳转路径、携带参数、目标类、拦截器状态等全部统一收敛在一个对象里,方便 ARouter 在运行期逐步补全。真正的 Intent 构造是最后在 _navigation() 才发生。Postcard 是整个路由链的"中间态模型"。

WareHouse

Warehouse(意为"仓库")扮演着中央数据存储库 的角色。它负责在内存中集中管理所有通过注解(如@Route@Interceptor)注册的路由信息和拦截器信息,是连接编译时生成的索引和运行时实际跳转的关键枢纽,ARouter 能够正常工作的所有核心映射关系和关键对象 都在WareHouse中存放。

当在Application中调用ARouter.init(...)时,ARouter会扫描这些编译器生成的类。通过反射实例化它们,并调用其loadInto方法,将其中定义的映射关系(例如,组名"app"对应ARouter$$Group$$app.class)填充到Warehouse的静态Map中。具体存储内容如下表

存储区域 存储内容说明 关键作用与特点
routes 详细的路由表 。以路径(Path) 为 Key,对应的路由元信息(RouteMeta) 为 Value 。 这是路由跳转的最终依据 。当根据路径查找目标时,就是查询这个 Map。它采用按需加载机制,并非在初始化时就填充所有数据 。
groupsIndex 路由组索引 。以组名(Group) 为 Key,对应的路由组类(如 ARouter$$Group$$home.class 为 Value 。 这是实现按需加载的关键。初始化时只加载这个"组目录",当某个组下的路径第一次被访问时,才动态加载整个组的所有路由到 routes中 。
providersIndex 服务提供者索引 。通常以接口的全类名 为 Key,对应的服务路由元信息(RouteMeta) 为 Value 。 记录服务接口与其实现类的映射关系,用于依赖查找和跨模块服务调用 。
providers 服务提供者单例缓存 。以服务接口的 Class 对象 为 Key,对应的已实例化的服务实现对象为 Value 。 缓存服务实例,确保服务全局唯一,避免重复创建 。
interceptorsIndex 拦截器索引 。以优先级数字 为 Key,对应的拦截器类(IInterceptor) 为 Value 。 记录所有拦截器及其优先级,在初始化时被加载 。
interceptors 拦截器实例列表 。在 ARouter 初始化完成后,根据 interceptorsIndex创建的拦截器对象列表 实际执行拦截逻辑的实例集合 。

编译时期

ARouter的编译期工作主要由arouter-compiler(编译时处理器)模块完成,是 ARouter 的最关键部分。其中包含了多个注解处理器,它们都继承自BaseProcessor

  • RouteProcessor : 处理 @Route注解,这是最核心的处理器,负责生成路由组(Group)和根(Root)映射表。
  • InterceptorProcessor : 处理 @Interceptor注解,生成拦截器映射表。
  • AutowiredProcessor : 处理 @Autowired注解,生成参数注入辅助类。

这里主要讲一下RouteProcessorRouteProcessor处理器会收集所有被@Route标注的元素(如Activity、Service等),并为每个元素创建一个RouteMeta对象。这个对象包含了该路由的所有关键信息,包括路径(Path) :注解中定义的路径,(如/app/main)。分组(Group) :如果注解中未显式指定,则从路径中提取(如/app/main的分组是app)。目标类(Destination) :被注解的类本身。路由类型(Type) :通过判断目标类是Activity、Service还是实现了IProvider接口等来确定其类型。

我们看下面这样的代码

kotlin 复制代码
@Route(path = "/home/main")
class HomeActivity : AppCompatActivity()

这里RouteProcessor实际做了三件事

  1. RouteProcessorprocess方法首先会扫描并收集所有被 @Route注解标记的类(如 Activity、Fragment 等)作为一个集合
typescript 复制代码
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
​
    if (CollectionUtils.isNotEmpty(annotations)) {
        Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
        parseRoutes(routeElements);
    }
    return true;
}

扫描后将@Route注解的集合调用parseRoutes对集合每个元素进行处理

scss 复制代码
private void parseRoutes(Set<? extends Element> routeElements) {
    for (Element element : routeElements) {
        Route route = element.getAnnotation(Route.class);
        parseRoute(element, route);
    }
}

对于每个收集到的元素,它会在parseRoute方法中提取注解中的关键信息(如 pathgroup),并根据该元素的类型(是 Activity 还是实现了 IProvider接口的服务等),创建一个 RouteMeta对象,该RouteMeta对象包含了该路由节点的所有元数据 , 如下

ini 复制代码
RouteMeta meta = new RouteMeta(
        RouteType.ACTIVITY,
        classElement, 
        routeAnnotation.path(),
        routeAnnotation.group()
);

其中最重要的是三个信息:type (ACTIVITY / FRAGMENT / PROVIDER)、pathgroup

  1. 自动创建 Group / Root 类。当所有 @Route 都解析完后,处理器开始生成代码。ARouter 编译期会生成至少两类 java 文件:

(1)RouteGroup_xxx.java,描述某 group 下有哪些路由,负责在编译时生成并在运行时注册路由信息,导航时ARouter通过查找这些注册信息,找到目标类,最终完成跳转。

typescript 复制代码
public class ARouter$$Group$$home implements IRouteGroup {
​
    @Override
    public void loadInto(Map<String, RouteMeta> atlas) {
        atlas.put("/home/main", RouteMeta.build(
            RouteType.ACTIVITY, 
            MainActivity.class, 
            "/home/main", 
            "home"
        ));
    }
}
​

(2)RouteRoot_AROUTER.java,描述所有 group

typescript 复制代码
public class ARouter$$Root$$app implements IRouteRoot {
    public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
        routes.put("home", ARouter$$Group$$home.class);
    }
}

这两个Java文件共同构成路由表系统,都会被打包到 APK 的 dex 文件中,且固定放在 com.alibaba.android.arouter.routes 包下,为后面的跳转做准备。读者可能会疑惑为什么不直接建一张大表包含所有的路径,ARouter 选择按 group 拆成多个 Java 类,而不是生成一张巨大路由表直接包含所有的路径,是为了避免一次性加载所有路由造成不必要的启动成本。运行期只会按需加载 group,相当于将整张路由表分片加载,这就是 ARouter 的"延迟加载机制"。

初始化时期

当应用启动调用 ARouter.init(application)时,就进入了初始化阶段,其核心任务是将编译期生成的各类索引(编译期间通过注解处理器自动生成的一系列映射表,上面的两个java文件就是其中最重要的索引 )加载到内存中。该阶段的入口在init方法中

csharp 复制代码
public static void init(Application application) {
    ARouter.hasInit = true;
    LogisticsCenter.init(application);
}

通过该入口我们实际可以看出,初始化阶段的核心逻辑在 LogisticsCenter.init()方法中。

初始化的关键工作是:找到所有编译期生成的 Root 实现类(上面说的RouteRoot_AROUTER.java),并调用它们把 groups 注册到 Warehouse.groupsIndex。核心代码如下

ini 复制代码
public static void init(Context context) {
    ...
        Set<String> classNames = ClassUtils.getFileNameByPackageName(context, "com.alibaba.android.arouter.routes");
    ...
        for (String className : classNames) {
            if (className.startsWith("ARouter$$Root$$")) {
                Class<?> clazz = Class.forName(className, false, classLoader);
                IRouteRoot root = (IRouteRoot) clazz.newInstance();
                root.loadInto(Warehouse.groupsIndex);  // 填充 group到GroupClass
            }
        }
    }

这里Class<?> clazz = Class.forName(className, false, classLoader);用反射加载了编译期生成的路由表类,但是但这个 Class.forName() 不是大量反射并且整个过程只执行一次,所以性能几乎没有影响。

这里强调一下,初始化时只缓存了Group的索引 ,仅仅是将编译期生成的路由组索引信息缓存到 Warehouse.groupsIndex,相当于建立了一份总目录,以组名(Group) 为 Key,对应的路由组类(如 ARouter$$Group$$home.class 为 Value 。而每个Group下的具体路由信息,是在该组第一次被访问时才动态加载和缓存的 。这种"分组懒加载"的设计是ARouter在性能优化上的一个关键点,它确保了应用启动速度快,且运行时内存占用合理。

运行期

编译期生成了很多 ARouter$$Root$$*ARouter$$Group$$**$$ARouter$$AutowiredARouter$$Interceptors$$* 类;运行期通过加载这些生成类把路由索引(groupgroupClasspathRouteMeta)填入 WarehouseARouter.build(path) 构造 Postcardnavigation() 负责:确保路由已加载、处理拦截器、准备 Intent / 参数、执行跳转或返回 provider/fragment 等。运行期主要分为三大模块,构建 PostcardLogisticsCenter.prepareCard():加载 group 文件跳转与拦截器链

当我们调用 ARouter.getinstance().build() 时,其实是在创建一个明信片 Postcard 对象,withXXX()navigation() 等方法就是它的方法。我们看一下build()的源码

typescript 复制代码
public Postcard build(String path) {
    return new Postcard(path, extractGroup(path));
}

Postcard 继承了 RouteMeta,包含了目的地址(路径、分组)以及后续可能添加的参数(Extras)等信息 。但此时明信片上的"收件人 "目标类 还是未知的。真正的核心逻辑继续看 navigation()方法。

java 复制代码
public Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    .......
    LogisticsCenter.completion(postcard); // 解析路由 / 加载 group
    .......
    // 执行拦截器链
    if (!postcard.isGreenChannel()) {
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            @Override
            public void onContinue(Postcard postcard) {
                _navigation()
            }
            @Override
            public void onInterrupt(Throwable throwable) {
                callback.onInterrupt()
            }
        });
    } else {
        return _navigation();
    }
}

其中代码LogisticsCenter.completion(postcard)根据路径找到对应的目标类,并将信息填充到Postcard中completion的代码如下,就是从缓存中通过 path查找 RouteMeta

ini 复制代码
public static void completion(Postcard postcard) {
    RouteMeta meta = Warehouse.routes.get(postcard.getPath());
    if (meta == null) {
        loadGroupIfNecessary(postcard.getGroup());
        meta = Warehouse.routes.get(postcard.getPath());
    }
    postcard.setRouteMeta(meta);
}

如果没有即meta == null,则延迟加载 group:loadGroupIfNecessary(),核心代码如下。

首先从 分组索引表 (Warehouse.groupsIndex) 中获取该组对应的"组加载器"的 Class 对象

接着通过反射 机制groupMeta.getConstructor().newInstance();将上一步获取到的 Class 对象 实例化,其中group.loadInto(Warehouse.routes)这个方法会一次性将 home分组下的所有 路由(包括 /home/main/home/detail)的 RouteMeta信息都添加到 Warehouse.routes缓存中。

最后再从分组索引表中将该group移除,因为该Group的路由信息已经加载到缓存了,下次再找目标类并填充到Postcard时直接就在缓存中找到了。

这个过程也用到了反射,因为前面初始化阶段为了性能考虑,仅仅缓存了以组名(Group) 为 Key,对应的路由组类(如 ARouter$$Group$$home.class 为 Value 的路由组信息,得不到实例类(想得到要么在运行前将所有的routes进行加载然后按需使用,要么对所有routes进行反射遍历,前者费空间后者又费时间)

ini 复制代码
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(groupName);
IRouteGroup group =  groupMeta.getConstructor().newInstance();
group.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(groupName);

当 Postcard 填充完 RouteMeta 后,ARouter 会继续执行拦截器链,即下面这段代码。

typescript 复制代码
   if (!postcard.isGreenChannel()) {
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            @Override
            public void onContinue(Postcard postcard) {
                _navigation()
            }
            @Override
            public void onInterrupt(Throwable throwable) {
                callback.onInterrupt()
            }
        });
    } else {
        return _navigation();
    }
}

这段代码的逻辑还是比较清楚的,如果这次跳转没有开启"绿色通道",拦截器链就会被启动。会进行链式执行ARouter会按照优先级顺序,依次调用每个拦截器的 process方法。每个拦截器在执行完自己的逻辑后,必须调用以下两个回调函数之一来决定路由的走向

  • callback.onContinue(postcard) :表示放行,流程将继续交给下一个拦截器。
  • callback.onInterrupt(throwable) :表示中断,整个跳转过程会立即停止,后续拦截器不再执行,并通过 NavigationCallback通知调用者跳转被拦截了。一旦有一个拦截器interrupt 整个跳转就终止,所有拦截器都通过后才执行真正的跳转

最终调用_navigation进行跳转,内部还是利用Intent跳转,结尾整个ARouter的流程

scss 复制代码
private Object _navigation() {
​
    switch(routeMeta.type) {
​
        case ACTIVITY:
            Intent intent = new Intent(context, routeMeta.getDestination());
            postcard.putExtras(intent);
            
            if (requestCode > 0) {
                ((Activity) context).startActivityForResult(intent, requestCode);
            } else {
                context.startActivity(intent);
            }
            break;
​
        case PROVIDER:
            return getOrCreateProvider(routeMeta);
        
        case FRAGMENT:
            Fragment fragment = routeMeta.getDestination().newInstance();
            fragment.setArguments(postcard.getExtras());
            return fragment;
​
        // 其他类型略...
    }
}
相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui