1. 从两个真实痛点说起为什么需要IProvider和IInterceptor做过Android组件化的人大概率都经历过这样的场景主App里想调用某个业务模块的能力比如用户模块的登录状态、订单模块的支付入口结果发现模块之间根本没有依赖关系直接引用对方的类会编译报错。另一种场景更常见——某个页面必须登录后才能进入你在每个Activity里都写一遍if (!isLogin) jumpToLogin()代码重复不说一旦登录逻辑有变动几十个地方都要改。这两个问题分别对应ARouter的两个核心能力IProvider服务调用解决的是跨模块拿服务的问题IInterceptor拦截器解决的是统一拦截处理的问题。很多人用ARouter只停留在ARouter.getInstance().build(/xxx/yyy).navigation()这个层面觉得能跳转就够了但真正到了中大型项目路由跳转只是最基础的一环服务发现和拦截器才是让组件化架构真正跑起来的关键。这篇文章我会把IProvider和IInterceptor这两块拆开揉碎讲清楚。不是简单贴API文档而是从为什么要这样设计到实际项目里怎么用不踩坑完整走一遍。适合已经用过ARouter基础跳转、想进一步把组件化做扎实的Android开发者。如果你还没接触过ARouter建议先了解一下基本的路由跳转用法再来看这篇理解会更顺畅。需要提前说明的是下面涉及的所有代码示例都是基于常见实践整理的通用写法具体版本API可能有细微差异以你项目实际引入的ARouter版本为准。2. IProvider服务调用的本质跨模块通信的接口下沉方案2.1 为什么不能直接依赖模块隔离的硬约束组件化项目里各业务模块之间是不允许直接互相依赖的。假设你有module-user和module-order两个模块module-order需要获取当前登录用户的userId如果直接在module-order的build.gradle里加上implementation project(:module-user)那这两个模块就耦合死了。更严重的是如果module-user也反过来依赖module-order直接循环依赖编译都过不了。那怎么办答案是把接口下沉到一个公共模块通常叫module-common或者module-api。module-user实现这个接口module-order只依赖接口不依赖实现。但问题来了module-order拿到了接口怎么拿到具体的实现实例这就是IProvider要解决的事情。用一句话概括IProvider的作用它让一个模块能够通过接口拿到另一个模块的实现实例而不需要在编译期建立任何依赖关系。这个思路和Java里的SPIService Provider Interface机制很像但ARouter把它做成了注解驱动用起来更顺手。2.2 接口下沉的具体操作从定义到实现先看接口怎么定义。在module-common里建一个接口继承IProviderpublic interface IUserService extends IProvider { boolean isLogin(); String getUserId(); String getUserName(); }这里有个细节值得注意接口必须继承IProvider因为ARouter在查找服务时需要调用init()方法来做初始化。IProvider接口本身只有一个init(Context context)方法你可以在里面做一些轻量的初始化工作但千万别在里面做耗时操作后面会详细说原因。然后在module-user模块里写实现类加上Route注解Route(path /user/service) public class UserServiceImpl implements IUserService { private Context mContext; Override public void init(Context context) { mContext context; } Override public boolean isLogin() { return UserManager.getInstance().isLogin(); } Override public String getUserId() { return UserManager.getInstance().getUserId(); } Override public String getUserName() { return UserManager.getInstance().getUserName(); } }Route注解的path就是服务的地址调用方通过这个path来找到实现。path的命名建议按模块划分前缀比如/user/、/order/、/pay/这样在路由表里一目了然排查问题的时候也方便。调用方怎么拿两种方式// 方式一通过path查找 IUserService userService (IUserService) ARouter.getInstance() .build(/user/service) .navigation(); // 方式二通过class查找ARouter 1.4.0支持 IUserService userService ARouter.getInstance() .navigation(IUserService.class);方式二更类型安全不需要强转推荐优先使用。但要注意方式二要求接口和实现类的对应关系能被ARouter正确识别如果遇到找不到的情况回退到方式一排查。2.3 服务调用的时机与生命周期陷阱这里有一个非常容易踩的坑ARouter的服务发现不是即时生效的它依赖于路由表的加载。在Application的onCreate()里初始化ARouter之后路由表并不是马上就可用的特别是在多模块、多Dex的情况下路由表的加载可能分散在不同的时机。我遇到过的情况是在Application.onCreate()里初始化ARouter后紧接着在某个模块的初始化逻辑里调用navigation(IUserService.class)结果返回null。原因就是那个模块的路由表还没加载完。解决办法有两种第一种是在Application.onCreate()里调用ARouter.openDebug()和ARouter.init()之后确保所有模块的初始化都在ARouter初始化完成之后再进行。可以通过在ARouter.init()的回调里做后续操作或者使用ARouter.getInstance().build(/xxx).navigation()时加降级策略。第二种更稳妥给服务调用加上空判断和降级逻辑。IUserService userService ARouter.getInstance().navigation(IUserService.class); if (userService null) { // 降级处理使用默认实现或者提示用户 userService new DefaultUserServiceImpl(); }注意init(Context context)方法在服务第一次被获取时调用之后不会重复调用。所以如果你在init()里保存了Context要注意这个Context的生命周期。建议使用context.getApplicationContext()来避免内存泄漏。另外IProvider默认是单例的。ARouter会缓存已经创建的服务实例下次再获取同一个path的服务时直接返回缓存实例。这个设计大多数时候是好事但如果你需要每次获取都创建新实例就需要在Route注解里设置Route(path /xxx, name xxx)不过ARouter本身对IProvider的单例控制比较固定如果需要非单例通常建议自己在实现类里做工厂方法。3. IInterceptor拦截器把登录校验从到处写变成写一次3.1 拦截器的执行模型优先级与拦截链IInterceptor的核心价值在于统一处理横切关注点。登录校验只是最典型的场景其他比如埋点统计、权限校验、页面重定向、参数预处理都可以通过拦截器来做。先看拦截器的基本写法Interceptor(priority 1, name 登录拦截器) public class LoginInterceptor implements IInterceptor { private Context mContext; Override public void process(Postcard postcard, InterceptorCallback callback) { String path postcard.getPath(); // 判断该页面是否需要登录 if (needLogin(path)) { if (isLogin()) { callback.onContinue(postcard); } else { // 未登录跳转到登录页 callback.onInterrupt(null); ARouter.getInstance().build(/login/activity) .withString(target_path, path) .navigation(); } } else { callback.onContinue(postcard); } } Override public void init(Context context) { mContext context; } }Interceptor注解有两个参数priority决定执行顺序数值越小越先执行name是描述性的方便调试时识别。拦截器的执行模型是一条链所有注册的拦截器按照priority排序依次执行。每个拦截器在process()方法里决定是继续callback.onContinue(postcard)还是中断callback.onInterrupt(exception)。一旦某个拦截器调用了onInterrupt后面的拦截器就不会再执行了。这个模型有一个关键点process()方法是在主线程调用的。如果你的拦截器里做了耗时操作比如读SP、查数据库会阻塞主线程。所以拦截器里的逻辑要尽量轻量重操作要么放到子线程要么提前缓存好结果。3.2 登录拦截的完整实现从判断到跳转再到回传登录拦截看起来简单但要做到体验流畅有几个细节需要处理好。第一个细节哪些页面需要登录有两种做法。一种是在拦截器里维护一个白名单或黑名单另一种是在Route注解里加额外的extras来标记。推荐后者因为标记和页面定义在一起不容易遗漏。Route(path /order/detail, extras 1) // extras 1 表示需要登录 public class OrderDetailActivity extends AppCompatActivity { // ... }然后在拦截器里通过postcard.getExtra()来获取这个标记Override public void process(Postcard postcard, InterceptorCallback callback) { if (postcard.getExtra() 1) { // 需要登录 if (isLogin()) { callback.onContinue(postcard); } else { // 跳转登录页并带上目标路径 callback.onInterrupt(null); ARouter.getInstance().build(/login/activity) .withString(target_path, postcard.getPath()) .withBundle(target_extras, postcard.getExtras()) .navigation(); } } else { callback.onContinue(postcard); } }第二个细节登录成功后怎么回到目标页面这是用户体验的关键。用户点了订单详情跳到登录页登录成功后应该自动跳到订单详情而不是让用户再点一次。在登录页的登录成功回调里String targetPath getIntent().getStringExtra(target_path); if (!TextUtils.isEmpty(targetPath)) { ARouter.getInstance().build(targetPath) .with(getIntent().getBundleExtra(target_extras)) .navigation(); finish(); }这里有个坑如果目标页面需要传递参数而你在拦截时只保存了path没保存参数回来的时候参数就丢了。所以上面代码里用withBundle把原始参数也带过去了。但要注意postcard.getExtras()返回的Bundle可能包含不可序列化的对象跨页面传递时要注意类型安全。第三个细节避免循环拦截。登录页本身不能需要登录否则会死循环。所以登录页的Route注解不要加extras 1或者在拦截器里对登录页的path做特殊判断直接放行。3.3 拦截器与IProvider的配合解耦登录状态判断拦截器里需要判断用户是否登录这个逻辑通常属于用户模块。如果拦截器直接依赖用户模块的类又回到了耦合的老问题。这时候IProvider就派上用场了Interceptor(priority 1, name 登录拦截器) public class LoginInterceptor implements IInterceptor { private IUserService userService; Override public void process(Postcard postcard, InterceptorCallback callback) { if (postcard.getExtra() 1) { if (userService null) { userService ARouter.getInstance().navigation(IUserService.class); } if (userService ! null userService.isLogin()) { callback.onContinue(postcard); } else { callback.onInterrupt(null); // 跳转登录页... } } else { callback.onContinue(postcard); } } Override public void init(Context context) { // 注意这里不要直接获取服务因为init时机可能早于路由表加载完成 } }注意上面代码里userService的获取放在了process()里而不是init()里。原因是拦截器的init()调用时机可能早于路由表完全加载在init()里获取服务可能拿到null。放在process()里虽然每次都要判断一下但可以确保路由表已经加载完成。如果担心性能可以在第一次获取成功后缓存起来。这种拦截器通过IProvider获取服务的模式是组件化项目里非常经典的解耦方式。拦截器本身放在一个基础模块里不依赖任何业务模块只依赖接口模块业务模块的实现通过ARouter注入进来。4. 那些文档里不会写的坑从实际项目中总结的避坑清单4.1 拦截器不生效的几种典型原因拦截器写了但没生效这是反馈最多的问题。排查下来通常有以下几个原因原因一拦截器没有被ARouter扫描到。ARouter的注解处理器在编译期生成路由表如果拦截器所在的模块没有被正确配置注解处理器或者模块本身没有被主App依赖拦截器就不会被注册。检查build.gradle里是否加了annotationProcessor com.alibaba:arouter-compiler:x.x.x如果是Kotlin项目要用kapt而不是annotationProcessor。原因二初始化时机不对。ARouter的初始化必须在拦截器使用之前完成。如果是在Application.onCreate()里初始化确保没有在其他更早的时机触发路由跳转。有些项目会在ContentProvider里做初始化时机更早但要注意多进程问题。原因三priority冲突或逻辑错误。如果两个拦截器priority相同执行顺序不确定。另外如果某个拦截器的process()里忘记调用callback.onContinue()或callback.onInterrupt()整个拦截链会卡住页面永远不会跳转。这个坑很隐蔽因为不会报错只是没反应。实操心得在拦截器的process()方法入口和出口都加上日志输出当前拦截器的name和postcard的path这样能快速定位是哪个拦截器卡住了。4.2 服务调用返回null的排查思路navigation(IUserService.class)返回null通常按以下顺序排查排查步骤检查内容常见问题1实现类是否有Route注解忘记加注解或path写错2实现类是否实现了IProvider接口只实现了自定义接口没继承IProvider3注解处理器是否配置build.gradle缺少annotationProcessor4模块是否被主App依赖模块没被依赖路由表不会合并5初始化时机ARouter.init()是否在调用之前完成6多Dex问题路由表是否在正确的Dex中加载其中第6条在多Dex项目中特别常见。ARouter的路由表类名有固定规则如果主Dex和从Dex的加载顺序有问题可能导致路由表找不到。解决办法是在build.gradle里配置multiDexKeepProguard把ARouter相关的类保留在主Dex中。4.3 拦截器里跳转登录页的时序问题在拦截器里调用callback.onInterrupt(null)之后立即跳转登录页这个顺序是有讲究的。如果先跳转再中断可能会出现目标页面已经启动、然后又被中断的混乱情况。正确的顺序是先onInterrupt再执行跳转。另外onInterrupt的参数是一个Exception虽然可以传null但建议传一个自定义异常方便在全局的NavigationCallback里区分是正常中断还是异常中断callback.onInterrupt(new LoginException(用户未登录));然后在NavigationCallback的onInterrupt回调里处理Override public void onInterrupt(Postcard postcard) { // 可以在这里做统一的埋点或提示 }4.4 拦截器中的Context使用注意事项拦截器的init(Context context)方法里拿到的Context在ARouter内部通常是Application级别的Context。但为了保险起见如果你需要保存Context建议统一转成ApplicationContextOverride public void init(Context context) { mContext context.getApplicationContext(); }这样即使ARouter在某些版本里传入了Activity的Context也不会造成内存泄漏。这个细节在大多数文档里不会提但实际项目中一旦泄漏排查起来非常麻烦。5. 进阶玩法让拦截器和IProvider发挥更大价值5.1 多拦截器协作登录、埋点、参数校验的分层设计实际项目里往往不止一个拦截器。合理的做法是按职责分层每层一个拦截器通过priority控制顺序priority 1登录拦截器最优先执行未登录直接中断priority 2埋点拦截器记录页面跳转事件priority 3参数校验拦截器检查必要参数是否缺失priority 4重定向拦截器处理旧路径到新路径的映射这种分层设计的好处是每个拦截器职责单一方便单独开启或关闭。比如在Debug环境下可以关闭埋点拦截器减少日志噪音。需要注意的是埋点拦截器应该在登录拦截器之后执行因为未登录的跳转不应该被埋点记录为目标页面的访问。而参数校验拦截器应该在登录之后因为未登录时参数可能还没准备好。5.2 IProvider的异步初始化解决耗时服务拖慢启动前面提到init()方法不要做耗时操作但有些服务确实需要异步初始化。比如用户服务可能需要从本地数据库读取用户信息这个操作不能放在主线程。解决方案是在init()里只做标记真正的初始化放到子线程然后通过回调或LiveData通知调用方Route(path /user/service) public class UserServiceImpl implements IUserService { private volatile boolean mInitialized false; private final CountDownLatch mLatch new CountDownLatch(1); Override public void init(Context context) { new Thread(() - { // 耗时初始化 loadUserFromDb(); mInitialized true; mLatch.countDown(); }).start(); } Override public boolean isLogin() { if (!mInitialized) { try { mLatch.await(2, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } return mLoginState; } }这种写法用CountDownLatch做同步等待但设置了超时时间避免无限阻塞。不过要注意如果isLogin()是在主线程调用的await()会阻塞主线程最多2秒体验不好。更好的做法是提供一个异步回调接口让调用方自己决定怎么处理等待。5.3 拦截器的动态开关Debug与Release的不同策略开发阶段可能希望某些拦截器不生效比如登录拦截器在调试时经常需要绕过。可以通过BuildConfig来做动态控制Override public void process(Postcard postcard, InterceptorCallback callback) { if (BuildConfig.DEBUG !ENABLE_LOGIN_INTERCEPTOR) { callback.onContinue(postcard); return; } // 正常拦截逻辑... }但要注意这种开关不要硬编码在代码里最好通过一个配置中心或本地SP来控制这样测试同学也能自己切换不用重新打包。5.4 服务降级与兜底当IProvider返回null时的优雅处理生产环境中IProvider返回null虽然不应该发生但一旦发生不能让App崩溃。建议对所有服务调用都做降级处理public class ServiceLocator { public static IUserService getUserService() { IUserService service ARouter.getInstance().navigation(IUserService.class); if (service null) { // 返回一个默认实现保证调用方不会NPE return new DefaultUserService(); } return service; } }DefaultUserService里所有方法返回安全的默认值比如isLogin()返回falsegetUserId()返回空字符串。这样即使服务发现失败App也能继续运行只是功能降级而不是直接崩溃。这种兜底思维在组件化项目里非常重要。模块之间的依赖越松散越需要考虑对方不存在的情况。ARouter提供了服务发现的能力但服务发现失败后的处理策略需要开发者自己设计。6. 一个完整的登录拦截服务调用实战流程把前面的内容串起来走一遍完整的流程。第一步在module-common里定义接口。public interface IUserService extends IProvider { boolean isLogin(); String getUserId(); void login(String userId, String userName); void logout(); }第二步在module-user里实现接口并注册服务。Route(path /user/service) public class UserServiceImpl implements IUserService { private String mUserId; private String mUserName; private boolean mIsLogin; Override public void init(Context context) { // 从本地存储恢复登录状态 mUserId SPUtils.getInstance().getString(user_id, ); mIsLogin !TextUtils.isEmpty(mUserId); } Override public boolean isLogin() { return mIsLogin; } Override public String getUserId() { return mUserId; } Override public void login(String userId, String userName) { mUserId userId; mUserName userName; mIsLogin true; SPUtils.getInstance().put(user_id, userId); } Override public void logout() { mUserId ; mUserName ; mIsLogin false; SPUtils.getInstance().remove(user_id); } }第三步在module-common里写登录拦截器。Interceptor(priority 1, name 登录拦截器) public class LoginInterceptor implements IInterceptor { private IUserService userService; Override public void process(Postcard postcard, InterceptorCallback callback) { if (postcard.getExtra() ! 1) { callback.onContinue(postcard); return; } if (userService null) { userService ARouter.getInstance().navigation(IUserService.class); } if (userService ! null userService.isLogin()) { callback.onContinue(postcard); } else { callback.onInterrupt(new RuntimeException(未登录)); ARouter.getInstance().build(/login/activity) .withString(target_path, postcard.getPath()) .withBundle(target_extras, postcard.getExtras()) .navigation(); } } Override public void init(Context context) { // 空实现服务在process中懒加载 } }第四步在需要登录的页面加标记。Route(path /order/detail, extras 1) public class OrderDetailActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 页面逻辑 } }第五步在登录页处理登录成功后的回跳。Route(path /login/activity) public class LoginActivity extends AppCompatActivity { private void onLoginSuccess(String userId, String userName) { IUserService userService ARouter.getInstance().navigation(IUserService.class); if (userService ! null) { userService.login(userId, userName); } String targetPath getIntent().getStringExtra(target_path); if (!TextUtils.isEmpty(targetPath)) { ARouter.getInstance().build(targetPath) .with(getIntent().getBundleExtra(target_extras)) .navigation(); } finish(); } }这个流程走下来登录拦截和服务调用的配合就完整了。拦截器负责拦服务负责判登录页负责回各司其职模块之间零直接依赖。我在实际项目中用这套方案跑了两年多最大的体会是拦截器的priority一定要规划好不要等到拦截器多了再调整顺序那时候牵一发而动全身。另外服务接口的设计要尽量稳定因为接口一旦发布所有依赖它的模块都会受影响改接口的成本比改实现高得多。建议在接口设计阶段就考虑好未来可能的需求扩展比如预留一些默认方法避免频繁修改接口定义。