
1. 组件化路由的核心命题拆解1.1 为什么需要路由框架做过中大型客户端项目的人都有一个共同感受模块一多Activity之间的跳转就成了噩梦。早期我们用显式IntentA模块要跳B模块的页面就得在A的build.gradle里依赖B或者把所有页面都塞进一个巨大的app模块里。前者导致模块间强耦合后者导致编译速度直线下降改一行代码等三分钟。组件化的核心诉求就是解耦——每个业务模块可以独立编译、独立运行模块之间不直接依赖。但解耦之后问题来了A模块想打开B模块的某个页面它连B的类都引用不到怎么跳这就是路由框架要解决的核心问题。ARouter的思路很直接每个模块在编译期把自己的页面信息注册到一张路由表里运行时通过一个统一的字符串路径来查找目标页面。A模块只需要知道路径字符串不需要知道B模块的任何类。路径就像门牌号你不需要认识房子里的人只要知道地址就能找过去。这个设计带来的好处是显而易见的。模块之间彻底解耦可以独立编译、独立测试页面跳转统一管理方便做拦截、降级、参数注入等切面逻辑跨模块通信有了标准化的通道不再需要各种hack手段。1.2 IProvider与IInterceptor的角色定位ARouter提供了两个非常关键的扩展点IProvider和IInterceptor。很多人刚开始用ARouter的时候只用了页面跳转功能觉得够用了。但真正把组件化做深之后会发现页面跳转只是最基础的需求跨模块的服务调用和统一的拦截逻辑才是让架构真正好用的关键。IProvider解决的是跨模块服务调用的问题。比如用户模块需要获取登录状态但登录逻辑在账户模块里。传统做法是定义一个公共接口放在base模块账户模块实现它用户模块通过某种方式拿到实现类的实例。ARouter的IProvider就是把这个过程标准化了你定义一个接口继承IProvider用注解标注路径然后通过ARouter.getInstance().build(path).navigation()就能拿到实例跟页面跳转的API风格完全一致。IInterceptor解决的是切面逻辑的问题。最典型的场景就是登录拦截某些页面或服务需要登录才能访问你不可能在每个页面的onCreate里都写一遍登录判断。用拦截器在路由跳转的过程中统一拦截判断是否需要登录、是否已登录未登录就重定向到登录页整个过程对业务代码零侵入。这两个东西配合起来用基本能覆盖组件化开发中80%的跨模块通信场景。下面我会从实际项目出发把这两个东西的用法、原理、坑点全部拆开讲清楚。2. IProvider服务调用的完整实现路径2.1 接口定义与实现的分离策略IProvider的使用遵循一个很经典的面向接口编程模式。具体来说分三步第一步在公共基础模块通常叫base或common里定义接口这个接口必须继承IProvider。比如我们要做一个账户服务public interface IAccountService extends IProvider { boolean isLoggedIn(); String getUserId(); void logout(); }注意这里接口继承IProvider是必须的因为ARouter在查找服务时需要用到IProvider接口里定义的方法。接口里只放业务方法签名不放任何实现细节。第二步在具体的业务模块比如account模块里实现这个接口并用Route注解标注路径Route(path /account/service) public class AccountServiceImpl implements IAccountService { Override public boolean isLoggedIn() { return UserManager.getInstance().isLogin(); } Override public String getUserId() { return UserManager.getInstance().getUserId(); } Override public void logout() { UserManager.getInstance().clear(); } Override public void init(Context context) { // 初始化逻辑ARouter在创建实例后会调用 } }第三步在其他模块里通过路径获取服务实例IAccountService accountService (IAccountService) ARouter.getInstance() .build(/account/service) .navigation();这个模式的关键在于base模块只依赖ARouter的annotation包不依赖任何业务模块业务模块依赖base模块调用方也只依赖base模块。整个依赖关系是单向的没有任何循环依赖。注意接口定义放在base模块时base模块需要添加ARouter的annotation依赖但不需要添加compiler依赖。compiler只在业务模块里配置。2.2 服务发现的底层机制ARouter的服务发现机制跟页面跳转共用一套路由表但查找逻辑略有不同。编译期ARouter的annotationProcessor会扫描所有带Route注解的类根据routeType区分是Activity还是IProvider分别生成不同的注册类。对于IProvider生成的注册信息里会包含目标类的Class对象。运行时ARouter根据路径找到对应的RouteMeta拿到Class对象后通过反射创建实例。这里有一个缓存机制第一次创建后会缓存实例后续获取同一个服务时直接返回缓存对象不会重复创建。但这里有个细节值得注意默认情况下IProvider的实现类是通过无参构造函数反射创建的。如果你的实现类没有无参构造函数就会crash。解决办法是在实现类里提供一个无参构造函数或者通过Route注解的extras参数传入构造参数但这种方式比较受限。另外IProvider接口里的init(Context context)方法会在实例创建后立即被调用你可以在这里做一些初始化工作。但要注意这个方法的调用时机是在路由查找过程中如果你在里面做了耗时操作会阻塞调用线程。2.3 服务调用的线程与生命周期管理IProvider实例的生命周期默认是单例的也就是说整个应用运行期间只会创建一个实例。这个设计有利有弊好处是省去了重复创建的开销坏处是你需要注意线程安全问题。如果你的服务实现类里有可变状态比如缓存了一些数据多线程同时访问就可能出问题。我一般建议IProvider的实现类尽量做成无状态的所有状态都委托给专门的Manager去管理。这样即使多线程调用也不会有问题。关于线程ARouter的服务查找默认在调用线程执行。如果你在主线程调用navigation()反射创建实例的过程就在主线程。虽然反射创建对象的耗时通常很短但如果你的init()方法里有耗时操作就会造成主线程卡顿。一个稳妥的做法是在Application初始化时提前调用一次navigation()把服务实例预热好后续使用就直接命中缓存了。// 在Application.onCreate中预热 ARouter.getInstance().build(/account/service).navigation();这个预热操作看起来不起眼但在实际项目中能避免不少因为首次调用导致的卡顿问题。3. IInterceptor登录拦截的落地实践3.1 拦截器的注册与优先级IInterceptor的使用比IProvider更简单一些但细节更多。定义一个拦截器Interceptor(priority 1, name 登录拦截器) public class LoginInterceptor implements IInterceptor { Override public void process(Postcard postcard, InterceptorCallback callback) { if (postcard.getPath().startsWith(/user/)) { if (!UserManager.getInstance().isLogin()) { // 未登录重定向到登录页 ARouter.getInstance().build(/login/activity) .withString(target, postcard.getPath()) .navigation(); callback.onInterrupt(null); return; } } callback.onContinue(postcard); } Override public void init(Context context) { // 拦截器初始化 } }Interceptor注解有两个参数priority和name。priority决定了拦截器的执行顺序数值越小越先执行。name只是个标识方便调试时看日志。这里有个很容易踩的坑priority的值不要重复。如果两个拦截器的priority相同执行顺序是不确定的可能导致逻辑混乱。我一般习惯按10、20、30这样的间隔来分配方便后续在中间插入新的拦截器。拦截器的执行流程是链式的ARouter会把所有拦截器按priority排序然后依次调用process方法。每个拦截器处理完后必须调用callback.onContinue()或callback.onInterrupt()否则整个路由流程会卡住。这一点非常关键我见过不止一个项目因为忘记调用callback导致页面跳转无响应。3.2 登录拦截的完整逻辑设计登录拦截看起来简单但要做好需要考虑不少细节。最基本的逻辑是判断目标路径是否需要登录如果需要且当前未登录就跳转到登录页。但实际项目中需求往往更复杂有些页面需要登录有些不需要登录后要自动跳回原来想去的页面登录页本身不能被拦截否则会死循环有些服务调用也需要登录校验我的做法是在拦截器里维护一个需要登录的路径前缀列表private static final String[] LOGIN_REQUIRED_PREFIXES { /user/, /order/, /settings/ };然后判断目标路径是否匹配这些前缀。对于登录页本身路径是/login/activity不在列表里自然不会被拦截。登录后跳回原页面的逻辑我是在跳转到登录页时把目标路径作为参数传过去登录成功后从参数里取出目标路径再跳一次// 登录成功后 String target getIntent().getStringExtra(target); if (!TextUtils.isEmpty(target)) { ARouter.getInstance().build(target).navigation(); finish(); }这个方案简单有效但要注意处理一种边界情况如果用户连续触发多次需要登录的跳转可能会打开多个登录页。解决办法是在拦截器里加一个标志位或者用singleTop启动模式。3.3 拦截器中的异步处理有些场景下登录状态的判断不是同步的。比如token可能存储在SharedPreferences里读取需要时间或者需要向服务端验证token是否有效。这时候拦截器的process方法里就需要做异步处理。ARouter的拦截器是支持异步的你可以在process方法里启动一个线程去做判断判断完再调用callback。但这里有个坑如果你在子线程调用了callback.onContinue()后续的路由跳转也会在子线程执行。如果目标是Activity在子线程启动Activity会crash。正确的做法是在子线程判断完后切回主线程再调用callbackOverride public void process(Postcard postcard, InterceptorCallback callback) { if (needLogin(postcard.getPath())) { new Thread(() - { boolean isLoggedIn checkLoginFromServer(); new Handler(Looper.getMainLooper()).post(() - { if (isLoggedIn) { callback.onContinue(postcard); } else { callback.onInterrupt(null); // 跳转登录页 } }); }).start(); } else { callback.onContinue(postcard); } }这个细节很容易被忽略但一旦踩到就是必现的crash排查起来还挺费时间的。4. 路由表生成与运行时查找原理4.1 注解处理器的编译期工作ARouter的性能之所以比纯反射方案好关键在于它在编译期就把路由表生成好了。具体来说ARouter的annotationProcessor在编译时会做以下几件事扫描所有带Route、Interceptor注解的类收集它们的路径、类型、Class对象等信息。然后按照模块分组为每个模块生成一个注册类类名通常是模块名加$$ARouter$$Group$$加组名。最后生成一个总的注册类把所有模块的注册类串起来。这些生成的类都实现了IRouteGroup或IInterceptorGroup接口里面的内容就是往RouteMeta里填充数据。运行时ARouter只需要加载这些生成的类就能拿到完整的路由表不需要遍历所有类去反射查找。这个设计的好处是查找效率极高基本就是一次HashMap查找的时间。而且因为路由表是编译期生成的如果路径写错了编译时就能发现ARouter的gradle插件会做路径校验不会等到运行时才crash。4.2 运行时查找的完整链路当调用ARouter.getInstance().build(path).navigation()时内部经历了这样几个步骤首先build(path)会创建一个Postcard对象解析路径中的参数。然后navigation()触发查找流程先查缓存如果之前查找过这个路径直接返回缓存的RouteMeta如果没有就去加载对应的注册类把路由信息加载到内存中。找到RouteMeta后根据routeType判断是Activity还是IProvider。如果是Activity就构建Intent并启动如果是IProvider就反射创建实例并返回。整个过程中拦截器是在查找完成后、实际跳转前执行的。ARouter会把所有拦截器按priority排序依次调用process方法。只有所有拦截器都调用了onContinue才会执行最终的跳转动作。这个链路看起来简单但实际使用中有几个性能相关的点值得注意首次查找某个路径时会触发注册类的加载这个过程涉及类加载和反射有一定开销。如果某个路径在冷启动时就被使用可能会影响启动速度。解决办法是在Application里提前调用一次ARouter.getInstance().build(path).navigation()做预热或者用ARouter的openDebug模式在开发阶段观察路由表的加载情况。5. 高频踩坑与排查实录5.1 路径重复与找不到路径路径重复是ARouter最常见的问题之一。两个不同的类用了相同的Route路径编译时可能不会报错取决于ARouter版本和配置但运行时只有一个能生效另一个会被覆盖。排查方法是开启ARouter的debug模式在Application里加上if (BuildConfig.DEBUG) { ARouter.openLog(); ARouter.openDebug(); }开启后路由表的加载过程会打印到Logcat哪条路径被覆盖了一目了然。找不到路径的问题通常有几个原因路径拼写错误、模块没有被正确依赖、annotationProcessor没有配置。特别是第三个如果业务模块的build.gradle里没有添加ARouter的compiler依赖注解处理器就不会工作路由表里就没有这个模块的信息。// 每个使用ARouter注解的模块都需要 annotationProcessor com.alibaba:arouter-compiler:1.5.2注意如果是Kotlin项目要用kapt代替annotationProcessor并且需要在kotlin-kapt插件之后配置。5.2 拦截器不生效的几种情况拦截器不生效最常见的原因是忘记在Application里初始化ARouter或者初始化时机太晚。ARouter的初始化必须在任何路由操作之前完成通常放在Application.onCreate()的最前面。另一个常见原因是拦截器的priority设置有问题。如果两个拦截器的priority相同执行顺序不确定可能导致你的登录拦截器在某个拦截器之后执行而那个拦截器已经调用了onInterrupt你的拦截器就不会被执行了。还有一种情况是拦截器里抛了异常但没有捕获导致整个拦截链断掉。ARouter的拦截器执行是在一个循环里如果某个拦截器的process方法抛了未捕获的异常后续拦截器不会执行路由跳转也会失败。所以拦截器里的代码一定要做好异常处理。5.3 服务调用返回null的排查思路通过IProvider获取服务时返回null通常有以下几个原因第一接口没有继承IProvider。ARouter在查找服务时会检查目标类是否实现了IProvider接口如果没有就返回null。第二实现类没有加Route注解或者注解的路径跟查找时用的路径不一致。第三实现类没有无参构造函数。前面提到过ARouter默认通过无参构造函数反射创建实例如果没有无参构造函数创建会失败。第四模块没有被正确初始化。如果实现类所在的模块没有被Application依赖路由表里就没有这个模块的信息自然找不到。排查的时候我一般会先开启ARouter的日志看查找过程中打印的信息。如果日志显示找到了RouteMeta但创建实例失败那就是构造函数的问题如果日志显示根本没找到RouteMeta那就是路径或模块依赖的问题。5.4 常见问题速查表问题现象可能原因排查方法跳转无响应拦截器未调用callback检查所有拦截器的process方法服务返回null接口未继承IProvider检查接口定义服务返回null实现类无无参构造函数添加无参构造函数拦截器不执行priority重复或初始化时机晚检查priority值和初始化位置路径找不到compiler依赖未配置检查build.gradle子线程启动Activity崩溃拦截器中异步调用callback切回主线程再调用callback登录后未跳回原页面未传递target参数检查登录页的参数处理6. 架构层面的经验与建议6.1 接口粒度与模块划分IProvider的接口粒度是一个需要仔细考虑的问题。接口太粗一个接口里塞了几十个方法实现类会变得很臃肿而且任何模块依赖这个接口都会引入不必要的依赖。接口太细又会造成接口数量爆炸管理成本上升。我的经验是按业务领域划分接口每个接口只负责一个内聚的功能集。比如账户服务一个接口订单服务一个接口支付服务一个接口。接口里的方法控制在10个以内超过就考虑拆分。另外接口定义放在base模块时要注意base模块不要变得太臃肿。如果base模块依赖了太多东西所有业务模块都会被迫依赖这些不必要的东西。一个做法是把接口按领域拆到不同的base子模块里业务模块按需依赖。6.2 拦截器的分层设计拦截器不要都写在一个类里按职责分层。我一般会分三层基础拦截器日志、埋点、业务拦截器登录、权限、降级拦截器兜底处理。每层用不同的priority区间比如基础层用1-99业务层用100-199降级层用900-999。这样分层的好处是逻辑清晰排查问题的时候也容易定位。而且不同层的拦截器可以独立开关比如开发阶段可以关掉登录拦截方便调试。6.3 降级策略的设计路由跳转失败是必须考虑的情况。目标页面不存在、模块被裁剪、版本不兼容都可能导致跳转失败。ARouter提供了降级服务接口可以全局设置一个降级处理器ARouter.getInstance().setDegradeService(new DegradeServiceImpl());在降级处理器里你可以根据失败的路径做不同的处理跳转到统一的错误页、显示Toast提示、或者跳转到Web页面兜底。这个机制在大型项目里非常重要能避免因为某个模块的问题导致整个应用不可用。7. 实际项目中的组合应用7.1 登录拦截与服务调用的配合在实际项目中登录拦截和IProvider服务调用经常需要配合使用。比如用户模块需要调用订单模块的服务查询订单列表但订单服务需要登录才能调用。这时候有两种做法一种是在拦截器里统一处理所有需要登录的服务路径都加上前缀拦截器统一判断。另一种是在服务实现类里自己判断登录状态未登录就返回空结果或抛出特定异常。我倾向于第一种做法因为拦截逻辑集中在一处便于维护。但要注意服务调用的拦截和页面跳转的拦截在ARouter里是同一套拦截器机制所以拦截器里需要区分当前是页面跳转还是服务调用。可以通过postcard.getType()来判断返回RouteType.ACTIVITY就是页面返回RouteType.PROVIDER就是服务。7.2 跨模块通信的完整案例假设我们有一个电商应用分为首页模块、商品模块、订单模块、账户模块。首页需要展示推荐商品点击商品跳转到商品详情下单需要登录订单列表需要登录。在这个场景下路由的配置大概是这样的商品详情页路径是/product/detail订单列表路径是/order/list登录页路径是/account/login。拦截器配置需要登录的前缀是/order/和/account/除了登录页本身。商品模块提供一个IProductService接口首页模块通过它获取推荐商品数据。订单模块提供一个IOrderService接口账户模块通过它获取当前用户的订单统计。这些服务接口都定义在base模块里实现类在各自的业务模块里。整个架构的依赖关系是base模块不依赖任何业务模块业务模块依赖base模块app模块依赖所有业务模块。模块之间通过ARouter的路径和服务接口通信没有任何直接依赖。这样的架构可以做到每个业务模块独立编译、独立运行开发效率会有明显提升。7.3 性能优化与监控ARouter本身性能已经不错但在大型项目里还是有一些优化空间。首先是路由表的懒加载ARouter默认是在第一次查找时才加载对应的注册类这个策略在大多数情况下是合理的。但如果某些路径在冷启动时就被频繁使用可以考虑在Application里提前加载。其次是拦截器的执行效率。拦截器是在主线程同步执行的如果拦截器里有耗时操作会直接影响跳转速度。我一般会限制拦截器里的操作只做必要的判断耗时的逻辑放到异步线程或者提前缓存好。监控方面可以重写ARouter的Logger接口把路由跳转的耗时、成功率等数据上报到监控平台。这样能及时发现路由相关的问题比如某个路径的跳转失败率突然升高可能是对应的模块出了问题。8. 一些容易忽略的细节8.1 参数传递的类型安全ARouter的参数传递是通过withString、withInt等方法设置的接收端通过getIntent().getStringExtra()等方式获取。这种方式没有类型检查如果发送端传的是String接收端按Int取就会出问题。一个改进做法是定义一个参数常量类发送端和接收端都引用这些常量避免拼写错误。更进一步可以定义一个参数构建器把参数的设置和读取封装起来提供类型安全的API。8.2 混淆配置ARouter在混淆环境下需要额外的配置否则生成的注册类可能被混淆掉导致路由表加载失败。需要在proguard-rules.pro里添加-keep public class com.alibaba.android.arouter.routes.**{*;} -keep class * implements com.alibaba.android.arouter.facade.template.ISyringe{*;}如果是使用IProvider的项目还需要保留IProvider的实现类-keep class * implements com.alibaba.android.arouter.facade.template.IProvider{*;}这个配置很容易被忽略因为debug模式下不混淆一切正常只有release包才会暴露问题。8.3 多模块下的初始化顺序在多模块项目里ARouter的初始化必须在所有模块的路由注册之前完成。通常是在Application的onCreate里调用ARouter.init()但要注意如果某个模块在Application.onCreate之前就触发了路由操作比如ContentProvider的onCreate就会出问题。解决办法是把ARouter的初始化提前到Application的attachBaseContext里或者用ContentProvider的方式自动初始化。ARouter本身提供了一个ARouterContentProvider在AndroidManifest里注册后会自动完成初始化不需要手动调用init()。我在实际项目里踩过最深的坑是拦截器里做异步登录校验时忘了切回主线程导致线上偶现崩溃。这个问题的难点在于它不是必现的只在特定时序下才会触发排查花了不少时间。后来养成了一个习惯拦截器里任何涉及线程切换的地方都加一行日志打印当前线程名这样一旦出问题看日志就能快速定位。另外ARouter的debug日志在开发阶段一定要开着它会把路由查找、拦截器执行、跳转结果都打印出来排查问题的效率能提升好几倍。