组件化方案:JIMU之UI路由(二)

前言

在上一篇组件化方案: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(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
    routeMapper.put("/uirouter/demo/4",Demo4Activity.class);
    paramsMapper.put(Demo4Activity.class,new java.util.HashMap(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
    routeMapper.put("/uirouter/demo/5",Demo5Activity.class);
    paramsMapper.put(Demo5Activity.class,new java.util.HashMap(){{put("foo", 9); }});
    routeMapper.put("/uirouter/demo/2",Demo2Activity.class);
    paramsMapper.put(Demo2Activity.class,new java.util.HashMap(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
    routeMapper.put("/uirouter/demo/8",Demo8Activity.class);
    paramsMapper.put(Demo8Activity.class,new java.util.HashMap(){{put("foo", 8); }});
    routeMapper.put("/uirouter/demo/7",Demo7Activity.class);
    paramsMapper.put(Demo7Activity.class,new java.util.HashMap(){{put("foo", 8); }});
    routeMapper.put("/uirouter/demo/6",Demo6Activity.class);
    paramsMapper.put(Demo6Activity.class,new java.util.HashMap(){{put("EXTRA_OBJ_FOO", 10); }});
    routeMapper.put("/uirouter/demo/1",Demo1Activity.class);
    routeMapper.put("/uirouter/demo",UiRouterDemoActivity.class);
  }
}

我们将路由信息存入了routeMapper:

protected Map routeMapper = new HashMap<>();

最终回归到系统API:

 if (routeMapper.containsKey(path)) {
            Class target = routeMapper.get(path);
            if (bundle == null) {
                bundle = new Bundle();
            }
            Map params = UriUtils.parseParams(uri);
            Map 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(){{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 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使用的困惑,再更新本文做更详细的展开

--

之前放了讨论群的链接居然被禁了。。尴尬。

你可能感兴趣的:(组件化方案:JIMU之UI路由(二))