经过前面三篇文章,相信大家对组件化都有了一定程度的理解。
在这个过程中一直强调了组件化的一个基础设施:路由!没有它组件化可以说是寸步难行,本篇文章我们就来谈谈一个组件化路由框架诞生过程中的那些思考。
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.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路由框架诞生过程中的思考,在下篇文章将会具体推荐一个路由框架。