Android 路由模块分析笔记

为什么需要路由模块

方便不同的模块间相互跳转,增加模块间的独立性,而显式的 startActivity 方式显然是做不到的,而且简洁优雅,当然还有其他更多的优点

需要考虑实现的基本功能

  • 1.ActivityFragment 的跳转

  • 2.跳转间的传参

  • 3.跳转 ActivitylauncherFlag 处理?

  • 4.转场动画?

开源库分析

ActivityRouter

特点:编译时注解不支持 Fragment多组件支持

实现
  • 路由映射

    映射关系通过 Router#map 方法添加,Mapping 类记录了某个 Page 的映射路径

    初始化,会在第一使用的时候完成,使用 @Module 后的每个模块内会生成一个 RouterMapping_xxx 的类来初始化当前模块的路由表,主模块还需要使用 @Modules 声明,并会生成一个 RouterInit 类以实现所有模块的

    主模块初始化的时候,调用各个模块初始化自己的路由表

    //APT 生成
    public final class RouterInit {
      public static final void init() {
        RouterMapping_app.map();
        RouterMapping_sdk.map();
      }
    }
    

    app 模块的路由表初始化

    //APT 生成
    public final class RouterMapping_app {
      public static final void map() {
        //路由表初始化
        //...
        //@Router(value = "home/:homeName")
        com.github.mzule.activityrouter.router.Routers.map("home/:homeName", HomeActivity.class, null, extraTypes);
        //@Router("with_host")
        com.github.mzule.activityrouter.router.Routers.map("with_host", HostActivity.class, null, extraTypes);
        //@Router(value = {"http://mzule.com/main", "main", "home"})
        com.github.mzule.activityrouter.router.Routers.map("http://mzule.com/main", MainActivity.class, null, extraTypes);
        com.github.mzule.activityrouter.router.Routers.map("main", MainActivity.class, null, extraTypes);
        com.github.mzule.activityrouter.router.Routers.map("home", MainActivity.class, null, extraTypes);
        //...
        }
      }
    
    • 启动页面

    最后通过 Routers#doOpen 来启动一个 Activity

    private static boolean doOpen(Context context, Uri uri, int requestCode) {
      initIfNeed();
      Path path = Path.create(uri);
      for (Mapping mapping : mappings) {
          if (mapping.match(path)) {
              if (mapping.getActivity() == null) {
                  mapping.getMethod().invoke(context, mapping.parseExtras(uri));
                  return true;
              }
              Intent intent = new Intent(context, mapping.getActivity());
              //...
              if (requestCode >= 0) {
                //...
              } else {
                  context.startActivity(intent);
              }
              return true;
          }
      }
      return false;
    }
    
    • 参数传递

    可以通过 url 来传递一些基本类型的参数,如果需要传递对象类型的数据或者添加 launchFlag,只能通过 Router#resolve 方法来返回一个 Intent对象,这无疑打破的代码的简洁和优雅性,可以考虑构造一个新的对象来负责跳转和跳转前额外参数的添加职责(后面说的 Rabbit 就更合理且优雅)

    • 具体流程

      ActivityRouter启动页面流程.png
    • 类图

      ActivityRouter类图

Rabbits

特点:编译时注解拦截器mappings更新(似乎没什么用?)多组件支持Fragment 支持(需要自己扩展)

实现
  • page 的映射关系

    根据 mappingsxxx.json 路由表文件(每次初始化的时候都需要读取 json 文件,耗时会随着页面增多而增加),建立 uri = page 之间的联系 ,模块内使用了 @Page(name = xxx) 注解的 ActivityFragment,编译后会生成 Router 类,记录的模块内所有 page 的跳转所需要的目标参数( 目标 Fragment 的实例或者目标 Activityclass ),最后根据 uri 可以得出目标 page 名,再通过动态代理的方法来实现具体方法的反射调用

    mappings 表,指定了路径、参数和对应 page 名:

    {
    ...
    "mappings": {
    "demo://rabbits.kyleduo.com/test": "TEST",  
    "demo://rabbits.kyleduo.com/test/{testing}": "TEST",  //String 类型参数
    "demo://rabbits.kyleduo.com/second/{id:l}": "SECOND", //long 类型,key 为 id
    "demo://rabbits.kyleduo.com/crazy/{name:s}/{age:i}/{finish:b}/end": "CRAZY"
    }
    }
    

    APT 生成的 Router 类,最后通过反射的方式调用

    public final class Router {
    //...
    public static final SecondFragment routeSecond() {
      return new SecondFragment();
    }
    
    public static final Class routeTest() {
      return TestActivity.class;
    }
    
  • uri 的匹配,先整个 uri 区配

    Mappings.java
    
    static Target match(Uri uri) {
    //...uri 在没有 scheme 或者 host 的情况下会自动添加默认的
    uri = builder.build();
    Uri pureUri;
    //清除 fragment 和 query 片段
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
      pureUri = builder.fragment(null).query(null).build();
    } else {
      pureUri = builder.fragment(null).clearQuery().build();
    }
    
    // Try to completely match.
    String page = sMappingsGroup.getMappings().get(pureUri.toString());
    Bundle bundle = null;
    if (page == null) {
      // Deep match.
      bundle = new Bundle();
      page = deepMatch(pureUri, bundle);
    }
    bundle = parseParams(uri, bundle);  //query 片段的 key-value 存进 bundle
    //...
    if (page != null && !free) {
      // Match.
      Target target = new Target(uri);
      target.setPage(page);
      target.setExtras(bundle);
      target.setFlags(parseFlags(uri));
      return target;
    }
    }
    

    RESTful 风格的 uri 匹配

    private static String deepMatch(Uri pureUri, Bundle bundle) {
    Set<String> uris = sMappingsGroup.getMappings().keySet();
    String[] source = pureUri.toString().split("(://|/)");
    UriLoop:
    for (String uri : uris) {
      // Check match for each uri.
      String[] template = uri.split("(://|/)");
      if (!template[0].equals(source[0]) || template.length != source.length) {
          continue;
      }
      if (!template[1].equals(source[1]) && (sMappingsGroup.getAllowedHosts() == null || !sMappingsGroup.getAllowedHosts().contains(source[1]))) {
          continue;
      }
      // Compare each part, parse params.
      for (int i = 2; i < source.length; i++) {
          String s = source[i];
          String t = template[i];
          if (t.equals(s)) {
              continue;
          }
          // Check whether a param field.
          if (t.matches("\\{\\S+(:\\S+)?\\}")) {  // {id:l} or {testing}
              try {
                  formatParam(t, s, bundle);//基本参数类型的解析
              } catch (NumberFormatException e) {
                  continue UriLoop;
              }
              continue; //参数都能匹配上才使用该 uri
          }
          continue UriLoop;
      }
      return sMappingsGroup.getMappings().get(uri);
    }
    return null;
    }
    
  • 参数传递

    通过 url 来传参只能支持基本数据类型显然是不够用的,还需要提供额外的方式来做更多的处理,包括 intentFlag 的添加,Rabbits#to 方法的产物 AbstractNavigator 对象就可以处理更多的需求

  • 具体流程

    Rabbit页面启动.png
  • 类图

    Rabbit类图

小结

Rabbit 相对 ActivityRouter 来说功能更多,更具有可扩展性,Rabbit 的路由表需要从 json 文件中解析,这种方式来管理虽然可能使得路由看起来更清晰且可以动态更新,但毕竟是耗时操作,还是有些小顾虑,作者也提供了一个异步的加载方法,但我觉得可以考虑使用 ActivityRouter 的方式来实现更好,纯靠 APT 来生成路由映射关系

更多

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容