货拉拉 Android 模块化路由框架:TheRouter

TheRouter 是一个 Kotlin 编写,用于 Android 模块化开发的一整套处理计划结构。
Github 项目地址与运用文档详见 github.com/HuolalaTech…。

TheRouter 中心功能具备如下才干:

  • 页面导航跳转才干(Navigator)
  • 跨模块依靠注入才干(ServiceProvider)
  • 单模块主动初始化才干(FlowTaskExecutor)
  • 动态化才干(ActionManager)
  • 模块AAR/源码依靠一键切换脚本

一、为什么要运用 TheRouter

路由是现如今移动端开发中必不可少的功能,尤其是企业级APP,能够用于将Intent页面跳转的强依靠联系解耦,一起削减跨团队开发的互相依靠问题。

关于大型 APP 开发,根本都会选用模块化(或组件化)办法开发,关于模块间解耦要求更高。 TheRouter 是一整套彻底面向模块化开发的处理计划,不仅能支撑常规的模块依靠解耦、页面跳转,一起供给了模块化进程中常见问题的处理办法。例如:完美处理了模块化开发后由于组件内无法获取 Application 生命周期与事务流程,造成每次初始化与关联依靠调用都需求跨模块修正代码的问题。

1.1 TheRouter 四大才干

Navigator:

  • 支撑 ActivityFragment
  • 支撑Path与页面多对一联系或1对1联系,可用于处理多端path一致问题
  • 页面Path支撑正则表达式声明
  • 支撑 json 格局路由表导出
  • 支撑动态下发 json 路由表,降级恣意页面为H5
  • 支撑恣意object跨模块传递(无需序列化,且能确保方针类型)
  • 支撑页面跳转阻拦处理
  • 支撑自定义页面参数解析办法(例如将json解析为方针)
  • 支撑运用路由跳转到第三方 SDK 中的Activity(Fragment)

ServiceProvider:

  • 支撑跨模块依靠注入
  • 支撑自定义注入项的创立规矩,依靠注入可自定义参数
  • 支撑自定义服务阻拦,单模块mock调试
  • 支撑注入方针缓存,屡次注入 只会new一次方针

FlowTaskExecutor:

  • 支撑单模块独立初始化
  • 支撑懒加载初始化
  • 独立初始化答应多使命依靠(参阅Gradle Task)
  • 支撑编译期循环引证检测
  • 支撑自定义事务初始化机遇,能够用于处理隐私合规问题

ActionManager:

  • 支撑大局回调装备
  • 支撑优先级呼应与中止呼应
  • 支撑记录调用途径,处理调试期观察者形式无法追寻Observable的问题

注: FlowTaskExecutorActionManager 后续会作为可选才干,供给可插拔单独运用的选项(预计10月份供给)。

二、路由计划

现在现有的路由根本上集中于两种才干的完成:页面跳转、跨模块调用,中心技术计划大体上如图:

货拉拉 Android 模块化路由框架:TheRouter

  1. 开发阶段,对要运用路由的落地页或被调用办法增加注解标识。
  2. 编译期解析注解,生成一系列中间代码,待调用。
  3. 运用发动后调用中间代码完成路由的预备动作。大部分路由会额定经过 Gradle Transform,在编译期做一次聚合,以提高运行时预备路由表的功率。
  4. 发起路由跳转时,本质上便是一次路由表遍历,经过uri获取到对应的落地页或办法方针,进行调用。

TheRouter 的页面跳转、跨模块调用也是如此,可是在规划上会有一些细节处理。

货拉拉 Android 模块化路由框架:TheRouter

TheRouter 会在编译期依据注解生成 RouteMap__开头的类,这些类中记录了当时模块的一切路由信息,也便是当时模块的路由表。

在最顶层的app模块中,经过Gradle插件,将一切aar、源码中的RouteMap__开头的类一致集中到TheRouterServiceProvideInjecter类中。

后续运用发动后,初始化路由时只需求履行TheRouterServiceProvideInjecter类的办法,就能没有任何反射的加载到悉数的路由表了。

加载今后的路由表会被保存到一个支撑正则匹配的 Map 中,这也是TheRouter答应多个path对应同一个落地页的原因。每逢产生页面跳转时,经过跳转时的path,去Map中获取到对应的落地页信息,再正常调用startActivity()即可。

三、运用 TheRouter 页面跳转

3.1 声明路由项

假如一个页面(支撑 Activity、Fragment)答应被路由翻开,则需求运用注解 @Route 声明路由项,每个页面答应声明多个路由项,也便是一对多的才干,极大下降多端路由一致时的事务影响面。

参数释义

  • path: 路由path 【必传】。
    主张是一个url。path内支撑运用正则表达式(为了匹配功率,正则有必要包括反双斜杠\),答应多个path对应同一个Activity(Fragment)。
  • action: 自定义事情【可选】。
    一般用来翻开方针页面后做一个履行动作,例如自定义页面弹出广告弹窗。
  • description: 页面描绘【可选】。
    会被记录到路由表中,便利后期排查的时候知道每个path或Activity是什么事务。
  • params: 页面参数【可选】。
    主动写入intent中,答应写在路由表中动态下发修正默许值,或经过路由跳转时代码传入。
@Route(path = "http://therouter.com/home", action = "action://scheme.com",
       description = "第二个页面", params = {"hello", "world"})
public class HomeActivity extends AppCompatActivity {
}

3.2 发起页面跳转

传入的参数可所以 String 和8种根本数据类型、也可所以BundleSerializableParcelable方针,跟 Intent 传值规矩一致。
一起也支撑为本次跳转的 Intent 增加Flag/Uri/ClipData/identifier等事务特殊参数。

// 传入参数能够经过注解 @Autowired 解析成恣意类型,假如是方针主张传json
// context 参数假如不传或传 null,会主动运用 application 替换
TheRouter.build("http://therouter.com/home")
        .withInt("key1", 12345678)
        .withString("key2", "参数")
        .withBoolean("key3", false)
        .withSerializable("key4", object)
        .withObject("object", any) // 这个办法能够传递恣意方针,可是接收的当地方针类型需自行确保一致,不然会强转反常
        .navigation(context);
        // 假如传入 requestCode,默许运用startActivityForResult发动Activity
        .navigation(context, 123);
        // 假如要翻开的是fragment,需求运用
        .createFragment();

3.3 路由表生成规矩

假如两条路由的path、方针className彻底相同,则认为是同一条路由,不会考虑参数是否相同
路由表生成规矩:编译期依照如下次序取并集

掩盖规矩
依据如下次序,假如相同,后者能够掩盖前者的路由表规矩。

  1. 编译期解析注解生成路由表
  2. 首先取 事务模块 aar 中的路由表
  3. 再取 主app module 代码中的路由表
  4. 最后取 assets/RouteMap.json 文件中声明的路由表。
  • 假如编译期没有这个文件,会生成一份默许路由表放在这个目录内;假如有,会将路由表兼并。
  • 路由表生成时可装备是否启用查看路由合法性,判断方针页面是否存在,(warning/error)级别。
  1. 运行时线上动态下发的路由表
  • 路由表答应线上动态下发,将掩盖本地路由表,详见 【3.4 动态路由表的规划与运用】

假如编译期没有这个文件,会生成一份默许路由表放在这个目录内;假如有,会将路由表兼并,因此,关于没办法修正代码的第三方SDK内部,假如期望经过路由翻开,只需求手动在RouteMap.json文件中声明,就能经过路由翻开了。

3.4 动态路由表的规划与运用

TheRouter 的路由表是动态增加的,项目每次编译后,会在 apk 内生成一份当时 APP 的全量路由表,默许途径为:/assets/therouter/routeMap.json。这个路由表也可今后续经过长途下发的办法运用,例如远端能够针对不同的APP版别,下发不同的路由表到达装备意图。这样假如将来线上某些页面产生Crash,能够经过将这个页面的落地页替换为H5的办法,暂时处理这类问题。

有两种引荐的长途下发办法可供运用方挑选:

  1. 将打包体系与装备体系打通,每次新版别APP打包后主动将assets/目录中的装备文件上传到装备体系,下发给对应版别APP 。优点在于全主动不会出错。
  2. 装备体系无法打通,线上手动下发需求修正的路由项,由于 TheRouter 会主动用最新下发的路由项掩盖包内的路由项。优点在于准确,且流量资源占用小。

注:一旦你设置了自定义的InitTask,原结构内路由表初始化使命将不再履行,你需求自己处理找不到路由表时的兜底逻辑,一种主张的处理办法见如下代码。

// 此代码 有必要 在 Application.super.onCreate() 之前调用
RouteMap.setInitTask(new RouterMapInitTask() {
    /** 
     * 此办法履行在异步
     */
    @Override
    public void asyncInitRouteMap() {
        // 此处为纯事务逻辑,每家公司远端装备计划或许都不相同
        // 不主张每次都恳求网络,不然恳求网络的进程中,路由表是空的,或许造成APP无法跳转页面
        // 最好是优先加载本地,然后开异步线程加载远端装备
        String json = Connfig.doHttp("routeMap");
        // 主张加一个判断,假如远端装备拉取失败,运用包内装备做兜底计划,不然或许造成路由表反常
        if (!TextUtils.isEmpty(json)) {
            List<RouteItem> list = new Gson().fromJson(json, new TypeToken<List<RouteItem>>() {
            }.getType());
            // 主张远端下发路由表差异部分,用远端包掩盖本地更合理
            RouteMap.addRouteMap(list);
        } else {
            // 在异步履行TheRouter内部兜底路由表
            initRouteMap()
        }
    }
});

3.5 高档用法

TheRouter一起支撑更多页面跳转才干,详情可参阅项目文档【github.com/HuolalaTech…】:

  • 为第三方库里面的页面增加路由表,到达对某些页面降级替换的意图;
  • 推迟路由跳转(从Android 8开始,不能在后台发动页面);
  • 跳转进程阻拦器(总共四层,可依据实际需求运用);
  • 跳转结果回调;

四、跨模块依靠注入 ServiceProvider 的规划

关于模块化开发中跨模块的调用,咱们引荐选用 SOA(面向服务架构) 的规划办法,服务调用方与运用方彻底隔离,调用模块外的才干不需求重视才干的供给者是谁。
ServiceProvider 的中心规划思想也是这样的,现在服务间的调用协议选用接口的办法。当然,也能够兼容不经过接口下沉而是直接调用的状况。

货拉拉 Android 模块化路由框架:TheRouter

详细到 Android 侧便是 AIDL 相似的规划,只是要比AIDL开发简单许多:

  • 服务供给方担任供给服务,不需求关怀调用方是谁会在何时调用自己。
  • 服务的运用方只重视服务本身,不需求关怀这个服务是谁供给的,只需求只能服务能供给哪些才干即可。

例如上面的图片:拉拉需求运用录音的服务,小货则向外供给一个录音的服务,由TheRouterServiceProvider担任撮合。

4.1 服务运用方:拉拉

她无需关怀,IRecordService这个接口服务是谁供给的,他只需求知道自己需求运用这样的一个服务就行了。
注:假如没有供给服务的供给方,TheRouter.get()或许返回null

TheRouter.get(IRecordService::class.java)?.doRecord()

4.2 服务供给方:小货

服务供给方需求声明一个供给服务的办法,用@ServiceProvider注解标记。

  • 假如是 java,有必要是 public static 润饰
  • 假如是 kotlin,主张写成 top level 的函数
  • 办法名不限
/**
 * 办法名不限定,恣意名字都行
 * 返回值有必要是服务接口名,假如是完成了服务的子类,需求加上returnType限定(例如下面代码)
 * 办法有必要加上 public static 润饰,不然编译期就会报错
 */
@ServiceProvider
public static IRecordService test() {
    return new IRecordService() {
        @Override
        public void doRecord() {
            String str = "履行录制逻辑";
        }
    };
}
// 也能够直接返回方针,然后标注这个办法的服名是什么
@ServiceProvider(returnType = IRecordService.class)
public static RecordServiceImpl test() {
    // xxx 
}

五、单模块主动初始化才干 FlowTaskExecutor 的规划

前面讲过,TheRouter是彻底面向模块化开发供给的一套处理计划。在模块化开发时,或许每个模块都有自己需求初始化的一些代码。以前的做法是把这些代码都在Application里声明,可是这样或许随着事务变动每次都需求修正Application地点模块。TheRouter 的单模块主动初始化才干便是为了处理这样的状况,能够只在当时模块声明初始化办法后,将会在事务场景时主动被调用。

每个期望被主动初始化的办法,有必要运用public static润饰,主要原因是这姿态就能经过类名直接调用了。另外许多初始化代码都需求获取Context方针,所以咱们将Context作为初始化办法的默许参数,会主动传入Application。其他的地点类名、办法名都没有约束,横竖只要加上了 @FlowTask 注解,在编译期都能经过 APT 获取到。

5.1 FlowTaskExecutor 运用介绍

能够在当时模块中,恣意类中声明一个恣意办法名的办法,给办法增加上@FlowTask 的注解即可。

@FlowTask 注解参数阐明:

  • taskName:当时初始化使命的使命名,有必要大局仅有,主张格局为:moduleName_taskName
  • dependsOn:参阅Gradle Task,使命与使命之间或许会有依靠联系。假如当时使命需求依靠其他使命先初始化,则在这儿声明依靠的使命名。能够一起依靠多个使命,用英文逗号分隔,空格可选,会被过滤:dependsOn = “mmkv, config, login”,默许为空,运用发动就被调用
  • async:是否要在异步履行此使命,默许false。
/**
 * 将会在异步履行
 */
@FlowTask(taskName = "mmkv_init", dependsOn = TheRouterFlowTask.APP_ONCREATE, async = true)
public static void test2(Context context) {
    System.out.println("异步=========Application onCreate后履行");
}
@FlowTask(taskName = "app1")
public static void test3(Context context) {
    System.out.println("main线程=========运用发动就会履行");
}
/**
 * 将会在主线程初始化
 */
@FlowTask(taskName = "test", dependsOn = "mmkv,app1")
public static void test3(Context context) {
    System.out.println("main线程=========在app1和mmkv两个使命都履行今后才会被履行");
}

5.2内置初始化节点

运用这个才干,在路由内部默许支撑了两个生命周期类使命,可在运用时直接引证

  • TheRouterFlowTask.APP_ONCREATE:当Application的onCreate()履行后初始化
  • TheRouterFlowTask.APP_ONSPLASH:当运用的首个Activity.onCreate()履行后初始化

一起,运用TheRouter的主动初始化依靠,也无需担心循环依靠造成的问题,结构会在编译期构建有向无环图,监测循环依靠状况,假如发现会在编译期直接报错,而且还会将产生循环引证的使命显示出来,用于排错。

5.3 完成原理

每个加了 @FlowTask 注解的办法,都会在编译期被解析,生成一个对应的 Task 方针,这个方针包括了初始化办法的相关信息,比如:是否异步履行、使命名、是否依靠其他使命先履行。

当一切aar都编译完成,生成好悉数的 Task 今后,会在主 app 中经过Gradle插件进行聚合,在这时会将一切的 Task 做一次查看,经过构建有向无环图来防止 Task 产生循环引证的状况。

每次运用发动后,会在路由初始化时,将有向图中的悉数Task,依照依靠联系按次序加载。

货拉拉 Android 模块化路由框架:TheRouter

六、动态化才干 ActionManager 的规划

Action 本质是一个大局的体系回调,主要用于预埋的一系列操作,例如:弹窗、上传日志、整理缓存。
与 Android 体系自带的广播通知相似,你能够在任何当地声明动作与处理办法。而且一切Action都是能够被盯梢的,只要你愿意,能够在日志中将一切的动作调用栈输出,以便利调试运用,这样在一定程度上能够处理观察者形式带来的通病:无法追寻Observable的问题

6.1 Action 运用

声明一个 Action:

// action主张遵从一定的格局
const val ACTION = "therouter://action/xxx"
@FlowTask(taskName="action_demo")
fun init(context: Context) =
    TheRouter.addActionInterceptor(ACTION, object: ActionInterceptor() {
        override fun handle(context: Context, args: Bundle): Boolean {
            // do something
            return false
        }
    })

履行一个 Action:

// action主张遵从一定的格局
const val ACTION = "therouter://action/xxx"
// 假如履行了一个没有被声明的Action,则不会有任何动作
TheRouter.build(ACTION).action()

6.2 高档用法

每个Action 答应关联多个 ActionInterceptor进行处理,多个ActionInterceptor之间能够自定义阻拦器优先级,一起答应终止接下来的低优先级阻拦器的履行。

最典型运用场景:主页或许会有多个弹窗,不同事务之间的弹窗是有优先级之分的,为了体验优化咱们必定不会在主页一次把一切弹窗悉数弹出,能够经过ActionInterceptor为每个弹窗声明好优先级联系,假设需求是主页只能弹出3个弹窗,那么第三个弹窗处理完毕即可关闭当时事情,接下来的阻拦器将不会被呼应。

abstract class ActionInterceptor {
    abstract fun handle(context: Context, args: Bundle): Boolean
    fun onFinish() {}
    /**
     * 数字越大,优先级越高
     */
    open val priority: Int
        get() = 5
}

6.3 客户端动态呼应运用场景

假如仅客户端运用,常用的场景或许是:当用户履行某些操作(翻开某个页面、H5点击某个按钮、动态页面装备的点击事情)时,将会主动触发,履行预埋的 Action 逻辑。

假如与服务端链路打通,这个才干其实是需求整个公司的配合,比如有一套相似才智大脑的计划,能够基于客户端过去的一些埋点数据,智能推断出用户下一步要做的事情,然后经过长连接直接向客户端下发指令做某些事情。那么经过客户端预埋的页面跳转、弹窗、清缓存、退出登录等等操作,就能够经过服务端指令进行操作,则便是一套完整的动态化计划。

货拉拉 Android 模块化路由框架:TheRouter

七、一键切换源码与 AAR

7.1 模块化支撑的 Gradle 脚本

在模块化开发进程中,假如没有选用分仓,或选用了分仓但仍然运用 git-submodule 的办法开发,应该都会遇到一个问题。假如集成包选用源码编译,构建时刻真实太久,大大下降开发调试功率;假如选用aar依靠编译,关于底层模块修正了代码,每次都要从头构建aar,在上层模块修正版别号今后,才干持续整包构建编译,也极大影响开发功率。
TheRouter 中供给了一个 Gradle 脚本,只需求在开发本地的local.properties文件中声明要参加编译的module,其他未声明的默许运用aar编译,这样就能灵敏切换源码与aar,而且不会影响其他人,如下节选代码可供参阅运用:

/**
 * 假如工程中有源码,则依靠源码,不然依靠aar
 */
def moduleApi(String compileStr, Closure configureClosure) {
    String[] temp = compileStr.split(":")
    String group = temp[0]
    String artifactid = temp[1]
    String version = temp[2]
    Set<String> includeModule = new HashSet<>()
    rootProject.getAllprojects().each {
        if (it != rootProject) includeModule.add(it.name)
    }
    if (includeModule.contains(artifactid)) {
        println(project.name + "源码依靠:===project(\":$artifactid\")")
        projects.project.dependencies.add("api", project(':' + artifactid), configureClosure)
//        projects.project.configurations { compile.exclude group: group, module: artifactid }
    } else {
        println(project.name + "依靠:=======$group:$artifactid:$version")
        projects.project.dependencies.add("api", "$group:$artifactid:$version", configureClosure)
    }
}

在实际运用时,能够彻底运用moduleApi 替换掉原有的api。当然, implementation也能够有一个对应的moduleImplementation,这样只需求注释或解注释setting.gradle文件内的include句子就能够到达切换源码、aar的意图了。

八、从其他路由搬迁至 TheRouter

8.1 搬迁东西一键搬迁

TheRouter供给了图形化界面的搬迁东西,能够一键从其他路由搬迁到TheRouter,现在仅支撑ARouter,其他路由结构搬迁也在开发中(GitHub下载,70M左右,请耐心等候):

  • Mac OS 搬迁东西下载:github.com/HuolalaTech…
  • Windows 搬迁东西下载:github.com/HuolalaTech…

假如项目中运用了ARouter的IProvider.init()办法,或许需求手动处理初始化逻辑。
如下图:

货拉拉 Android 模块化路由框架:TheRouter

8.2 与其他路由比照

功能 TheRouter ARouter WMRouter
Fragment路由 ✔️ ✔️ ✔️
支撑依靠注入 ✔️ ✔️ ✔️
加载路由表 无运行时扫描
无反射
运行时扫描dex
反射实例类
性能损耗大
运行时读文件
反射实例类
性能损耗中
注解正则表达式 ✔️ ✖️ ✔️
Activity指定阻拦器 ✔️(四大阻拦器可依据事务定制) ✖️ ✔️
导出路由文档 ✔️(路由文档支撑增加注释描绘) ✔️ ✖️
动态注册路由信息 ✔️ ✔️ ✖️
APT支撑增量编译 ✔️ ✔️(开启文档生成则无法增量编译) ✖️
plugin支撑增量编译 ✔️ ✖️ ✖️
多 Path 对应同一页面(低成本完成双端path一致) ✔️ ✖️ ✖️
远端路由表下发 ✔️ ✖️ ✖️
支撑单模块独立初始化 ✔️ ✖️ ✖️
支撑运用路由翻开第三方库页面 ✔️ ✖️ ✖️
支撑运用路由翻开第三方库页面 ✔️ ✖️ ✖️
对热修正支撑(例如tinker) ✔️(未改动的代码屡次构建无变动) ✖️(屡次构建apt产物会产生变化,生成无意义补丁) ✖️(屡次构建apt产物会产生变化,生成无意义补丁)

九、总结

TheRouter 并不仅仅是一个细巧灵敏的路由库,而是一整套完整的 Android 模块化处理计划,能够处理几乎悉数的模块化进程中会遇到的问题。
关于现有的路由结构,咱们也在最大极限支撑滑润搬迁,现在已完成ARouter的一键搬迁东西,其他结构的搬迁仍在开发中。你也能够在Github issue中提出需求,咱们评价后会赶快支撑,也欢迎任何人供给 Pull Requests

更多问题请访问:详细交流