关于android:货拉拉开源模块化路由框架TheRouter

7次阅读

共计 10518 个字符,预计需要花费 27 分钟才能阅读完成。

TheRouter 是一个 Kotlin 编写,用于 Android 模块化开发的一整套解决方案框架。
Github 我的项目地址与应用文档详见 https://github.com/HuolalaTech/hll-wp-therouter-android。

TheRouter 外围性能具备如下能力:

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

一、为什么要应用 TheRouter

路由是现如今挪动端开发中必不可少的性能,尤其是企业级 APP,能够用于将 Intent 页面跳转的强依赖关系解耦,同时缩小跨团队开发的相互依赖问题。

对于大型 APP 开发,根本都会选用模块化 (或组件化) 形式开发,对于模块间解耦要求更高。TheRouter 是一整套齐全面向模块化开发的解决方案,不仅能反对惯例的模块依赖解耦、页面跳转,同时提供了模块化过程中常见问题的解决办法。例如:完满解决了模块化开发后因为组件内无奈获取 Application 生命周期与业务流程,造成每次初始化与关联依赖调用都须要跨模块批改代码的问题。

1.1 TheRouter 四大能力

Navigator:

  • 反对 ActivityFragment
  • 反对 Path 与页面多对一关系或一对一关系,可用于解决多端 path 对立问题
  • 页面 Path 反对正则表达式申明
  • 反对 json 格局路由表导出
  • 反对动静下发 json 路由表,降级任意页面为 H5
  • 反对任意 object 跨模块传递(无需序列化,且能保障对象类型)
  • 反对页面跳转拦挡解决
  • 反对自定义页面参数解析形式(例如将 json 解析为对象)
  • 反对应用路由跳转到第三方 SDK 中的Activity(Fragment)

ServiceProvider:

  • 反对跨模块依赖注入
  • 反对自定义注入项的创立规定,依赖注入可自定义参数
  • 反对自定义服务拦挡,单模块 mock 调试
  • 反对注入对象缓存,屡次注入 只会 new 一次对象

FlowTaskExecutor:

  • 反对单模块独立初始化
  • 反对懒加载初始化
  • 独立初始化容许多任务依赖(参考Gradle Task)
  • 反对编译期循环援用检测
  • 反对自定义业务初始化机会,能够用于解决隐衷合规问题

ActionManager:

  • 反对全局回调配置
  • 反对优先级响应与中断响应
  • 反对记录调用门路,解决调试期观察者模式无奈追踪 Observable 的问题

注: FlowTaskExecutorActionManager 后续会作为可选能力,提供 可插拔 独自应用 的选项(预计 10 月份提供)。

二、路由计划

目前现有的路由基本上集中于两种能力的实现:页面跳转、跨模块调用,核心技术计划大体上如图:

  1. 开发阶段,对要应用路由的落地页或被调用办法增加注解标识。
  2. 编译期解析注解,生成一系列中间代码,待调用。
  3. 利用启动后调用中间代码实现路由的筹备动作。大部分路由会额定通过 Gradle Transform,在编译期做一次聚合,以晋升运行时筹备路由表的效率。
  4. 发动路由跳转时,实质上就是一次路由表遍历,通过 uri 获取到对应的落地页或办法对象,进行调用。

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 种根本数据类型、也能够是 BundleSerializable
Parcelable 对象,跟 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)级别。
  5. 运行时线上动静下发的路由表

    • 路由表容许线上动静下发,将笼罩本地路由表,详见【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 同时反对更多页面跳转能力,详情可参考我的项目文档【https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/Navigator】:

  • 为第三方库外面的页面增加路由表,达到对某些页面降级替换的目标;
  • 提早路由跳转(从 Android 8 开始,不能在后盾启动页面);
  • 跳转过程拦截器(总共四层,可依据理论需要应用);
  • 跳转后果回调;

四、跨模块依赖注入 ServiceProvider 的设计

对于模块化开发中跨模块的调用,咱们举荐采纳 SOA(面向服务架构) 的设计形式,服务调用方与应用方齐全隔离,调用模块外的能力不须要关注能力的提供者是谁。
ServiceProvider 的外围设计思维也是这样的,目前服务间的调用协定采纳接口的形式。当然,也能够兼容不通过接口下沉而是间接调用的状况。

具体到 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,依照依赖关系按程序加载。

六、动态化能力 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 逻辑。

如果与服务端链路买通,这个能力其实是须要整个公司的配合,比方有一套相似智慧大脑的计划,能够基于客户端过来的一些埋点数据,智能推断出用户下一步要做的事件,而后通过长连贯间接向客户端下发指令做某些事件。那么通过客户端预埋的页面跳转、弹窗、清缓存、退出登录等等操作,就能够通过服务端指令进行操作,则就是一套残缺的动态化计划。

七、一键切换源码与 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 迁徙工具下载:https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/uploads/file/TheRouterTransfer-Mac.zip
  • Windows 迁徙工具下载:https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/uploads/file/TheRouterTransfer-Windows.zip

如果我的项目中应用了 ARouter 的 IProvider.init()办法,可能须要手动解决初始化逻辑。
如下图:

8.2 与其余路由比照

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

九、总结

TheRouter 并不仅仅是一个玲珑灵便的路由库,而是一整套残缺的 Android 模块化解决方案,可能解决简直全副的模块化过程中会遇到的问题。
对于现有的路由框架,咱们也在最大限度反对平滑迁徙,目前已实现 ARouter 的一键迁徙工具,其余框架的迁徙仍在开发中。你也能够在 Github issue 中提出需要,咱们评估后会尽快反对,也欢送任何人提供 Pull Requests

更多问题请拜访:具体沟通

正文完
 0