前言
在上一篇组件化方案:JIMU之UI路由(一)中,我们简单介绍了JIMU中用于UI跳转的UIRouter,较为扼要的阐述了它存在的原因,如何集成,如何使用基础功能以及一些常见的排错。
这一篇中,会较为细致的阐述其原理,功能特性。
原理
如果不使用HOOK技术,那么此类路由最终都会回归到系统API
-- Leobert
在展开讨论之前,一定要记住我上面这句话,无论是谁给出的UI路由方案,在Activity跳转上,最终都会体现为
Context#startActivity(Intent intent)
JIMU中的实情
仔细了解过JIMU的朋友们都知道:JIMU是编译期隔离、运行期不隔离的。这一点决定了我们不需要使用ClassLoader,我们需要做的是采用一种映射技术:在编译期自动创建映射、按照映射编写可以通过编译的代码,在运行期遵循映射执行相应的逻辑。这样就满足了组件化的核心:“隔离与发现”。
严格的数学上的映射:两个非空集合A与B间存在着对应关系f,而且对于A中的每一个元素x,B中总有有唯一的一个元素y与它对应,就这种对应为从A到B的映射,记作f:A→B。
而且假如B到A也满足该条件,就是一种特殊的情况:一一映射(或称双射)
而我设计的UIRouter是一种一一映射的场景:Enum(host+path) <--> Enum(Activity)。
为什么废弃了UIRouter中priority的设计:我们在编码阶段所知的是host和path,需要根据它找到Activity,所以是Enum(host+path) --> Enum(Activity),而加入priority,则升维成:Enum(host+path+priority) --> Enum(Activity)。这并不是无法实现,而是增加了集成和使用的成本,加入priority之后,按照设计常理,不适合做成双射。一般而言会按照Chain of Responsibility进行设计,对priority做范围划分,这就破坏了设计的初衷
实现host+path到Activity的映射
在UIRouter中,我们使用RouteNode注解来为Activity指定他的Path,而Host根据Module环境确定,一个Module中,最终生成一张路由表,例如:
public class AppUiRouter extends BaseCompRouter {
@Override
public String getHost() {
return "app";
}
@Override
public void initMap() {
super.initMap();
routeMapper.put("/main",MainActivity.class);
routeMapper.put("/uirouter/demo/3",Demo3Activity.class);
paramsMapper.put(Demo3Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
routeMapper.put("/uirouter/demo/4",Demo4Activity.class);
paramsMapper.put(Demo4Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
routeMapper.put("/uirouter/demo/5",Demo5Activity.class);
paramsMapper.put(Demo5Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 9); }});
routeMapper.put("/uirouter/demo/2",Demo2Activity.class);
paramsMapper.put(Demo2Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
routeMapper.put("/uirouter/demo/8",Demo8Activity.class);
paramsMapper.put(Demo8Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); }});
routeMapper.put("/uirouter/demo/7",Demo7Activity.class);
paramsMapper.put(Demo7Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); }});
routeMapper.put("/uirouter/demo/6",Demo6Activity.class);
paramsMapper.put(Demo6Activity.class,new java.util.HashMap<String, Integer>(){{put("EXTRA_OBJ_FOO", 10); }});
routeMapper.put("/uirouter/demo/1",Demo1Activity.class);
routeMapper.put("/uirouter/demo",UiRouterDemoActivity.class);
}
}
我们将路由信息存入了routeMapper:
protected Map<String, Class> routeMapper = new HashMap<>();
最终回归到系统API:
if (routeMapper.containsKey(path)) {
Class target = routeMapper.get(path);
if (bundle == null) {
bundle = new Bundle();
}
Map<String, String> params = UriUtils.parseParams(uri);
Map<String, Integer> paramsType = paramsMapper.get(target);
UriUtils.setBundleValue(bundle, params, paramsType);
Intent intent = new Intent(context, target);
intent.putExtras(bundle);
if (requestCode > 0 && context instanceof Activity) {
((Activity) context).startActivityForResult(intent, requestCode);
return true;
}
context.startActivity(intent);
return true;
}
如何处理参数?
再回忆一下我开始说的内容,一定会回归到系统API,通过Intent跳转Activity时,我们携带参数是利用了Bundle,这里也不会例外。
JIMU中,可以直接使用Bundle携带参数以及像往常一样从Intent中获取参数。但是这还不够。
实际情景中,为了扩大APP体量以及利用社交享受社交红利,会利用社交媒体分享mobile-web站点页面,为了给用户更沉浸的体验以及引导转化,会要求从mobile-web站点回流到APP,也就是我们常说的web唤醒
而可行的技术就是通过向系统注册APP可以处理某类数据(特定的uri)然后从web中发出处理相应uri的请求。
从UX的角度来说,当然希望用户继续按照刚才的页面操作下去,这时候一个问题摆在眼前:如何通过Uri传递参数给相应的Activity?
从之前文章中的讨论以及一些常识,我们知道外部唤醒使用的是一个特定协议的Url。传递参数的问题就变为:
如何使用Url包装入参?
我们知道,一个Url可以拆解为以下部分
[schema]//[host]:[port][path][queryString][#hash]
可以用于包装参数的无非:
- path
- queryString
而框架中采用的是queryString。
理由:
path已经用于映射,在path中采用参数表达式不够自由且容易出bug,例如PHP的一款支持restful风格的框架laravel就在其路由匹配上出现过各种bug;
qs足够清晰明了。
从queryString到Bundle
在回忆一下前文提到的:“一定会回归到系统API”,Activity想要获取Bundle参数,一定会牵涉到类型,向Bundle写入时,也会牵涉到类型。故而:一定需要知道需要的参数的类型。Autowired注解应运而生。
所以利用注解生成了如下的参数关系:
paramsMapper.put(Demo6Activity.class,new java.util.HashMap<String, Integer>(){{put("EXTRA_OBJ_FOO", 10); }});
"EXTRA_OBJ_FOO"是Bundle的key,10代表了参数类型。过多的细节不做深入。需要注意的是,JIMU目前支持以下类型:
- 基本类型
- 基本类型对应的装箱类型
- String
- Parcelable接口实现类
- Object,通过Gson(最老版本使用了fastjson)对Object进行序列化和反序列化。
注意,不支持其他的Serializable的实现类,例如,Foo类就是不可以的:
public class Foo implement Serializable {
public String bar;
}
对比以下两个Bundle类的API,就会理解原因
public <T extends Parcelable> T getParcelable(@Nullable String key)
public Serializable getSerializable(@Nullable String key)
而JIMU中取参数赋值用的是对成员变量直接赋值:
substitute.foo = substitute.getIntent().getStringExtra("foo");
substitute.bar = substitute.getIntent().getStringExtra("EXTRA_STR_BAR");
注入代码生成的路径在一个包内:
package com.luojilab.componentdemo.router.cases;
/**
* Auto generated by AutowiredProcessor */
public class Demo2Activity$$Router$$Autowired implements ISyringe {}
package com.luojilab.componentdemo.router.cases;
@RouteNode(path = "/uirouter/demo/2", desc = "使用bundle传递参数")
public class Demo2Activity extends TestActivity {}
故而:Field需要声明为public或者default。
这里扯开一句,其实在去年我编写了一个完全由兴趣驱动的项目:MagicBox,用来简化以及规范项目中对InstanceState的处理,因为后续的商业项目都不是从零开始的,就没有考虑投入商业实战。在这个库中,几乎实现了Bundle支持的所有的参数的读写,默认值填充,不限制可见性修饰符,处理父类中的参数,处理引用对象内的参数,处理引用对象父类中声明的参数。但是我也不鼓励大家去研究这个项目,当时未考虑将其加入JIMU的原因有几点:JIMU中的方法已经足够了;MagicBox采用的反射,一定程度影响效率;MagicBox的使用更复杂。
UIRouter的一些特性
其实在前文中已经零零碎碎说了不少了。新版本中对Log输出的支持进行了大力改进。新版本的Demo代码也提供了相应的sample,如果本周没有合并到master分支的话,请关注dev分支。
sample:
- Demo1Activity 无参数跳转
- Demo2Activity 使用bundle传递参数
- Demo3Activity 使用Url传参
- Demo4Activity Url和Bundle同时包含参数,以url为准
- Demo5Activity Parcelable和Serializable
- Demo6Activity 使用json字符串传参
- Demo7Activity 必须参数,必须参数缺失,不使用抛出功能,可以看到log输出的信息。
- Demo8Activity 必须参数2,必须参数缺失,使用抛出异常功能,以及使用安全模式。
这里就不给大家详细展开了,Demo中也进行了一定的解释,大家也可以进行一些额外的测试,例如:url的path不匹配(错误的path),未加载mapping的情况下进行跳转,错误的参数值(无法parse,Json结构异常等)
关于外部唤醒
此次Demo中包含了一种特殊情况的处理策略,注意它并不是一个规范,只是一种特殊情况的可行性策略。不要引起误会。外部唤醒,需要实际情况实际分析,如有必要将通过issue进行讨论,并最终进行总结,不在本文展开。
PS:如果继续收到大家对UIRouter使用的困惑,再更新本文做更详细的展开
--
之前放了讨论群的链接居然被禁了。。尴尬。