经过前面三篇文章, 相信大家对组件化都有了一定程度的理解.
在这个过程中一直强调了组件化的一个基础设施: 路由! 没有它组件化可以说是寸步难行, 本篇文章我们就来谈谈一个组件化路由框架诞生过程中的那些思考.
1, 为什么需要路由
这个问题其实我们之前谈到过, 而且有过组件化实践或者尝试的同学一定有切身感受. 明确一个前提: 各个业务模块之间不会是相互隔离而是必然存在一些交互的;
在 Module A 需要跳转到 Module B 某界面, 而我们一般都是使用强引用的 Class 显式的调用;
在 Module A 需要调用 Module B 提供的方法, 例如别的 Module 调用用户模块退出登录的方法;
这两种调用形式大家很容易明白, 正常开发中大家也是毫不犹豫的调用. 但是我们在组件化开发的时候却有很大的问题:
模块 B 的 Activity Class 在自己的 Module B, 那 Module A 必然引用不到, 显式跳转行不通;
同理, 直接去调用某个 Module 的方法也行不通;
由此: 必然需要一种支持组件化需求的交互方式, 提供 UI 跳转以及方法调用的能力.
2, 一个路由库需要满足什么
首先这个路由库也是一个技术组件, 在整体组件化层次的设计中处于 Lib 层, 作为一项基础库. 那么路由库不仅仅需要满足自身的能力, 同时势必要满足一项基础库该有的条件:
Api 友好, 接入简单, 低成本;
具备 UI 跳转和方法调用的能力;
功能稳定;
可定制化;
3, 淘汰过的方式
任何系统或框架, 虽然在高版本中看起来都很完美, 但是实际上一开始并非就是如此, 都是一步步实践, 迭代改善到基于当前相对完美状态的. 比如我们之前就思考过如下方式:
3.1, 基于隐式意图
各位老司机都知道, Android 中打开一个 Activity, 可以有两种方式, 显示意图和隐式意图. 既然显式意图导致了强引用, 那么我们使用隐式意图, 既可以打开 Activity, 同时也不会造成 Module 间的强引用.
评价: 这种方式确实可以完成路由的 UI 跳转功能, 但是依赖于 Manifest 文件的修改, 同时参数也存在不便传递的问题, 因此不做推荐.
3.2, 基于事件, 使用广播或 EventBus
这种思路也很容易想到, 既然不能直接交互, 那么就隐式的来, 在需要交互的地方发通知, 然后接收方根据不同的通知类型做出不同的处理.
- /**
- * EventBus 的事件类
- */
- public class InteractEvent {
- public int type;
- public String param;// eg:String 类型参数一
- public ParamObject paramObject;// eg:Object 类型参数二
- }
- /**
- * 处理不同的交互设置
- * @param event
- */
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void onMessageEvent(InteractEvent event) {
- if(event.type == EventType.JUMP_LOGINACTIVITY){
- Intent intent = new Intent(mContext,LoginActivity.class);
- intent.putExtra("param",event.param);
- intent.putExtra("paramObject",event.paramObject);
- mContext.startActivity(intent);
- }
- }
评价: 这种交互的方式是可行的, 但是可以明显看出, 比较复杂, 对于界面跳转比较多的场景, 接入及维护成本较高.
3.3, 调用一个固定的方法
我们在需要交互的类中加上方法, 方法签名固定, 然后给交互类打上一个标签. 这样在别的组件中我们需要这个交互类的时候通过标签拿到, 调用这个固定的方法. 思路是这样, 以下提供一种方式的伪代码, 有不同的实现.
- public interface IDoAction{
- void doAction(HashMap hashMap);
- }
- public class LoginActivity extends Activity implements IDoAction{
- public void doAction(HashMap hashMap){
- // 1. 获取参数, Type
- hashMap.get();
- // 2. 跳转
- }
- }
- // 调用
- Dispatcher.get(url).doAction(hashMap);
备注使用 HashMap 作为参数的原因: 每个交互类需要的参数不一样, 而方法签名必须固定才能通过接口去调用, 传递 HashMap 这个参数可以包含多个不同类型的参数.
评价: 最不推荐, 使用繁琐, 侵入性太强, 改造成本极大.
4, 一种好的路由实践
总结以上几种不好的实践方式, 都在于侵入性强, 接入及维护成本高等. 那反过来推就是一个好的路由需要具备低侵入性, 易接入, 自动化等特性. 上述第三种方案我们可以吸收一点的是每个交互类打上一个标签, 记录这个映射关系, 方便在别的 Module 进行获取.
eg: easyrouter://main/Detail ---- MainDetailActivity
顺着这个映射往下想, 这个映射保存了标签和 Activity, 那么打开 Activity 只需要知道这个标签即可. 举例: 标签 A 和 ActivityA 对应, 那么我们只要遇到标签 A 就知道它想要打开的是 ActivityA. 同时如果我们处理好了打开 Activity 需要的传参问题就离自动化迈出了一大步.
问题就简化成了两个:
映射关系, 我们可以使用 String 字符串作为标签, 既保证通用性又可以保证唯一性. 利用一个 Map 保存这个字符串和 Activity 的映射关系, 这样可以保证在别的 Module 能通过字符串获取到我们需要的 Activity;
传参以及 Activity 各种特性 (利用动画, onActivityResult 生命周期) 的支持;
关于第二个问题实际上就是将这个字符串尽可能多的解析到 Android 多需要的数据, 比如参数传递, 动画, 生命周期等. 关于这个解析可以有两种方式:
直接简单粗暴在 String 后面拼一个参数, 这个参数的格式是 Json, 到达目标界面之后目标界面再去解析;
制定一定规则通过路由就解决好, 到目标界面直接像正常 Android 开发一样去获取;
eg: easyrouter://routertest?name=liuzhao&company=mycompany
5, 方法调用的实现
方法调用看起来都可以通过上述: 基于事件及调用一个固定的方法等方式来实现, 但是使用起来必定复杂无比, 各个 Module 之间交互不仅改造困难, 维护成本也很高.
注意各个 Module 向外提供的方法必定不一样: 需要不同的方法签名. 而且从改造及维护成本考虑, 最好可以像是在一个 Module 里一样直接调用, IDE 可以自动提示出来方法参数.
那我们就想把 Module 向外提供的方法内聚到一个类里, 只向外暴露这个类, 简称这个 Module 的交互服务类. 这样别的 Module 调用的时候就可以想直接调用普通类的方法一样简单方便了.
那我们就剩下一个问题: 别的 Module 如何获取你的交互服务类呢? 很容易想到上面提到的映射, 但是此种场景下如果使用字符串做 Key 真的可以吗? 如果使用字符串做 Key, 别的 Module 拿到的 Value 只能确定是一个 Class, 具体的 Class 类型却不清楚, 调用具体的方法尤其是被 IDE 提示, 更是不可能. 问题又简化成了如何让我们知道拿到的 Class 中有哪些方法呢?
经过多次思考, 终于想到了一个解决方案: Module 需要向外暴露的方法, 我们通过一个 Interface 来定义, 这个 Interface 定义在 Lib 层也就是说每个 Module 都可以访问到, 而保存映射关系的 Key 我们也使用这个 Interface. 那么映射表里保存的就是:
private static Map<Module 暴露接口 Interface, Module 暴露接口的实现类 InterfaceImpl> moduleInteracts = new HashMap<>();
那么别的 Module 在获取这个服务类时就可以直接通过在 Lib 层定义的 Interface 来获取, 然后通过泛型转换成这个接口, 而后直接调用相应方法即可, 就像调用一个普通方法一样简单:
- public static <T extends IBaseModuleService> T getModuleService(Class<T> tClass) {
- if (moduleInteracts.containsKey(tClass)) {
- return (T) moduleInteracts.get(tClass);
- }
- return null;
- }
调用: EasyRouter.getModuleService(BaseModuleService.ModuleInteractService.class).runModuleInteract(MainActivity.this);
6, 路由的最佳实践
6.1, 编译时注解
经过四, 五两节我们知道了路由相对较好的实践, 但同时我们能否让这个过程自动化呢? 其实可以借助编译时注解技术自动生成映射表, 这样在接入的时候就更加简单方便, 只需要在对应的 Activity 上打上一个注解, 配置相应的字符串, 这个映射表就自动生成.
- @DisPatcher("easyrouter://routertest")
- public class MainActivity extends Activity {
- ......
- }
生成代码
- @Override
- public void initActivityMap(HashMap<String, Class> activityMap) {
- activityMap.put("easyrouter://routertest", MainActivity.class);
- }
6.2, 拦截器
6.2.1, 统一判断
在实际开发中, 我们经常会遇到些统一的操作, 比如某些应用是需要用户先登陆的, 那么在用户浏览之后的下一步操作时用户进行各种点击都需要进行判断是否登陆, 未登录则跳转到登陆界面, 登陆之后则放行.
正常情况我们需要在每一个点击的地方进行判断, 但是明显费时费力, 既然我们已经做了路由, 所有的界面跳转都需要经过我们, 那我们自然可以进行统一的判断, 在路由进行分发时候进行判断, 满足拦截器条件则进行拦截.
6.2.2, 重定向
如果我们需要对 App 的功能进行 A/BTest, 我们该如何进行呢? 方式肯定有很多, 但是不一定通用. 注意我们已经有了路由, 结合路由来做 A/BTest 的话更加方便:
首先我们给路由加一个拦截器, 每一条跳转都会经过这个拦截器的判断;
通过路由实现界面跳转, 在路由解析过程中我们识别到了需要跳转的是 A 模块;
经过拦截器的判断, 如果 A/BTest 实验命中的是 B 模块, 则将这个路由进行重定向到 B 模块;
备注: 重定向的好处还有更多, 比如紧急情况下的热修复替换成 H5 界面等.
6.3, 过程监听
就是监听打开 Activity 的过程, 如
打开前进行数据的准备;
打开后的回调;
未匹配到目标 Activity 的降级等;
本文主要介绍一个 Android 路由框架诞生过程中的思考, 在下篇文章将会具体推荐一个路由框架.
来源: https://juejin.im/post/5ae2c5b0f265da0b722ad929