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

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

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

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据