TheRouter是什么

TheRouter是货拉拉开源的 Android 平台中对页面、服务模块化整合开发、提供路由性能的中间件,提倡的是简略且够用。

Github: https://github.com/HuolalaTech/hll-wp-therouter-android
官网: https://therouter.cn

简略应用示例

Activtiy跳转

TheRouter.build("http://therouter.cn/test/activity").withString("name", "姓名").navigation();

获取依赖服务

// 假如以后有一个用户信息获取服务public interface IUserService {    String getUserInfo();}// 服务提供方@ServiceProvider(returnType = IUserService.class)public static UserServiceImpl test() {    xxx}// 服务应用方TheRouter.get(IUserService::class.java)?.getUserInfo()

次要代码构造

TheRouter  ├─app  │   └──代码应用示例Demo  ├─business-a  │   └──用于模块化业务模块的演示模块  ├─business-b  │   └──用于模块化业务模块的演示模块  ├─business-base  │   └──用于模块化根底模块的演示模块  ├─apt  │   └──注解处理器相干代码  ├─plugin  │   └──编译期 Gradle 插件源码  └─router      └──路由库外围代码

外围源码剖析

TheRouter.build("http://therouter.cn/test/activity").withString("name", "姓名").navigation();

从最罕用的跳转开始剖析,根本可理解到 TheRouter 的运行原理。这行实现跳转的代码最终成果是携带参数跳转到对应的 Activity,在 Android 层面来说最初肯定是通过调用 startActivity 或是 startActivityForResult 来实现跳转。

分为几步来看:

  1. TheRouter 调用 Build 生成 Navigator,过程是怎么的
  2. Navigator 是什么
  3. Navigator 调用 navigation 是怎么执行到 startActivity 的

生成 Navigator

通过查看源码TheRouter.build()实际上调用的是Navigator的构造方法,

@JvmStaticfun build(url: String?): Navigator {    return Navigator(url)}

而在构造方法外面,能够看到代码的实现比较复杂,但要害局部都有正文,根本都能看得懂,次要能够分为两局部:

  • 拦截器批改url
  • 解析存储url上的参数
init {    require(!TextUtils.isEmpty(url), "Navigator", "Navigator constructor parameter url is empty")    for (handle in fixHandles) {        handle?.let {            url = it.fix(url)        }    }    uri = Uri.parse(url ?: "")    // queryParameterNames() 会主动decode,造成内部逻辑谬误,所以这里须要依据&手动截取k=v    uri.encodedQuery?.split("&")?.forEach {        val idx = it.indexOf("=")        val key = if (idx > 0) it.substring(0, idx) else it        val value: String? = if (idx > 0 && it.length > idx + 1) it.substring(idx + 1) else null        // 通过url取到的value,都认为是string,autowired解析的时候会做兼容        extras.putString(key, value)    }}

Navigator 是什么

依据 官网文档的介绍 https://therouter.cn/docs/2022/08/28/01
TheRouter的页面跳转都由 Navigator 导航器去操作的。

外部大抵能够分为四局部:

  • ur/path 的解析与填充
  • 路由表匹配
  • 执行跳转
  • 参数解析

参数的解析

我看的是1.1.1-rc1版本的代码,url的解析基本上就是通过uri去解析的。看到正文上,写了一个老版本的问题:

 uri = Uri.parse(url ?: "")//        for (key in uri.queryParameterNames) {//            // 通过url取到的value,都认为是string,autowired解析的时候会做兼容//            extras.putString(key, uri.getQueryParameter(key))//        }        // queryParameterNames() 会主动decode,造成内部逻辑谬误,所以这里须要依据&手动截取k=vuri.encodedQuery?.split("&")?.forEach {    val idx = it.indexOf("=")    val key = if (idx > 0) it.substring(0, idx) else it    val value: String? = if (idx > 0 && it.length > idx + 1) it.substring(idx + 1) else null    // 通过url取到的value,都认为是string,autowired解析的时候会做兼容    extras.putString(key, value)}

下面被正文掉的代码就是老版本的解析,正文写的很分明,因为uri.queryParameterNames() 会主动decode,造成内部逻辑谬误,所以新版本就改成了依据&手动截取k=v的形式做了。

在执行解析uri之前,其实还做了一次拦截器的替换。
依照官网文档:Path 修改器的利用场景是用于修复客户端上路由 path 谬误问题。

例如:相对路径转绝对路径,或因为服务端下发的链接无奈固定https或http,但客户端代码写死了 https 的 path,就能够用这种形式对立。

注:所有的拦截器必须在 TheRouter.build() 办法调用前增加处理器,否则解决前的path不会被批改。这个倒也正当。

for (handle in fixHandles) {    handle?.let {        url = it.fix(url)    }}

路由表

TheRouter是个动静路由,所以路由表其实被弱化为一个很泛的概念了。
感觉有很多套路由表,最终都会被汇总到一个反对正则表达式的Map外面。

首先讲路由表的创立起源,我能找到的就是这四种:

  • 从以后模块,通过 APT 解析@Route生成的
  • 从依赖 aar 的路由表中读取的
  • 从json文件中读取的
  • 代码增加的路由表

APT生成的路由表

先看第一种,最好了解的,就是注解处理器解析生成的。

@Route(path = "http://therouter.com/home", action = "action://scheme.com",        description = "第二个页面", params = {"hello", "world"})public class HomeActivity extends AppCompatActivity {}

参数释义

  • path: 路由path 【必传】。
    倡议是一个url。path内反对应用正则表达式(为了匹配效率,正则必须蕴含反双斜杠\),容许多个path对应同一个Activity(Fragment)。
  • action: 自定义事件【可选】。
    个别用来关上指标页面后做一个执行动作,例如自定义页面弹出广告弹窗
  • description: 页面形容【可选】。
    会被记录到路由表中,不便前期排查的时候晓得每个path或Activity是什么业务
  • params: 页面参数【可选】。
    主动写入intent中,容许写在路由表中动静下发批改默认值,或通过路由跳转时代码传入。

AAR依赖传递的路由表

所有的aar,只有是蕴含有路由表的,都会有一个叫 RouterMap__TheRouter__202xxxxx 最初面是一段 hashcode。

这个文件其实在源码编译的时候也能找到,在 build/source/kapt/debug/a/RouterMap__TheRouter__202xxxxx 外面。

这个文件蕴含两局部:

ROUTERMAP 是一个 json 格局的路由表,上面的addRoute 办法,是路由表的代码实现,这应该也是为什么 TheRouter 能号称无反射的起因。

public static final String ROUTERMAP = "[{\"path\":\"http://kymjs.com/business_a/testinject\",\"className\":\"com.therouter.demo.shell.TestInjectActivity\",\"action\":\"\",\"description\":\"\",\"params\":{}},{\"path\":\"http://kymjs.com/business_a/testinject3\",\"className\":\"com.therouter.demo.shell.TestActivity\",\"action\":\"\",\"description\":\"\",\"params\":{}},{\"path\":\"http://kymjs.com/business_a/testinject4\",\"className\":\"com.therouter.demo.shell.MultiThreadActivity\",\"action\":\"\",\"description\":\"\",\"params\":{}},{\"path\":\"http://kymjs.com/therouter/demo_service_provider\",\"className\":\"com.therouter.demo.shell.MainActivity\",\"action\":\"\",\"description\":\"\",\"params\":{}}]";public static void addRoute() {    com.therouter.router.RouteItem item1 = new com.therouter.router.RouteItem("http://kymjs.com/business_a/testinject","com.therouter.demo.shell.TestInjectActivity","","");    com.therouter.router.RouteMapKt.addRouteItem(item1);    com.therouter.router.RouteItem item2 = new com.therouter.router.RouteItem("http://kymjs.com/business_a/testinject3","com.therouter.demo.shell.TestActivity","","");    com.therouter.router.RouteMapKt.addRouteItem(item2);    com.therouter.router.RouteItem item3 = new com.therouter.router.RouteItem("http://kymjs.com/business_a/testinject4","com.therouter.demo.shell.MultiThreadActivity","","");    com.therouter.router.RouteMapKt.addRouteItem(item3);    com.therouter.router.RouteItem item4 = new com.therouter.router.RouteItem("http://kymjs.com/therouter/demo_service_provider","com.therouter.demo.shell.MainActivity","","");    com.therouter.router.RouteMapKt.addRouteItem(item4);}

从json文件读取的路由表

TheRouter我的项目每次编译后,会在apk内生成一份路由表,默认门路为:/assets/therouter/routeMap.json

同时这份路由表也反对远端动静下发,例如远端能够针对不同的APP版本,下发不同的路由表达到配置目标。因而有两种举荐的形式可供使用方抉择:

  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()        }    }});

执行跳转

执行跳转的次要办法总共有三个,别离是:跳转到Activity、获取跳转的Fragment、获取跳转的Intent。
次要逻辑都差不多,咱们次要看 Activity 的跳转。

代码很长,我就不全贴了。Activity 的跳转分五个局部:

  • 判断是否提早跳转
  • 拦截器解决
  • 解析跳转的路由表
  • 执行跳转
  • 跳转页面参数解析

提早跳转

提早跳转是个比拟翻新的设计,装置官网的说法,提早跳转次要利用场景有两种:

  • 第一种:初始化期间,如果路由表的量十分微小时。这种状况在别的路由框架上要么会白屏一段时间,要么间接抛弃这次跳转。在TheRouter中,框架会暂存以后的跳转动作,在路由表初始化实现后立即执行跳转。
  • 第二种:从Android 8.0开始,Activity 不能在后盾启动页面,这对于业务判断造成了很大的影响。因为可能会有前台 Service 的状况,不能单纯以 Activity 生命周期判断前后台。在TheRouter中,框架容许业务自定义前后台规定,如果为后盾状况,能够将跳转动作暂存,当进入前台后再复原跳转。
// 暂存的动作能够有多个,会在复原时按程序执行TheRouter.build("http://therouter.com/home")        .withInt("key1", 12345678)        .padding()// 暂存以后跳转动作        .navigation(context);        // 复原//toplevel办法,无需类名调用,Java请通过NavigatorKt类名调用sendPendingNavigator();   

又是拦截器

在这一步其实是有两个拦截器,一个是在路由表解析之前,一个是在路由表解析之后。
这里我就一起讲了。

  1. 路由表解析之前的,叫 页面替换器

利用场景:须要将某些path指定为新链接的时候应用。 也能够用在修复链接的场景,然而与 path 修改器不同的是,修改器通常是为了解决通用性的问题,替换器只在页面跳转时才会失效,更多是用来解决个性问题。

例如模块化的时候,首页壳模板组件中开发了一个SplashActivity广告组件作为利用的MainActivity,在闪屏广告完结的时候主动跳转业务首页页面。 然而每个业务不同,首页页面的 Path 也不雷同,而不心愿让每个业务线本人去改这个首页壳模板组件,此时就能够组件中先写占位符https://kymjs.com/splash/to/home,让接入方通过 Path 替换器解决。

Navigator.addPathReplaceInterceptor(new PathReplaceInterceptor() {    @Override    public String replace(String path) {        if ("https://kymjs.com/splash/to/home".equals(path)) {            return "https://kymjs.com/business/home";        }        return path;    }});

2 . 路由表解析之后的,叫 路由替换器

利用场景:罕用在未登录不能应用的页面上。例如拜访用户钱包页面,在钱包页申明的时候,能够在路由表上申明本页面是须要登录的,在路由跳转过程中,如果落地页是须要登录的,则先替换路由到登录页,同时将原落地页信息作为参数传给登录页,登录流程解决实现后能够继续执行之前的路由操作。

路由替换器的拦挡点更靠后,次要用于框架曾经从路由表中依据 path 找到路由当前,对找到的路由做操作。

这种逻辑在所有页面跳转前写不太适合,以前的做法通常是在落地页写逻辑判断用户是否具备权限,但其实在路由层实现更适合。

Navigator.addRouterReplaceInterceptor(new RouterReplaceInterceptor() {    @Override    public RouteItem replace(RouteItem routeItem) {        if (user.age() < 18 && routeItem.getClassName().contains("ChildrenProhibitActivity")) {            RouteItem target = new RouteItem();            target.setClassName(HomeActivity.class.getName());            String[] path = {"https://kymjs.com/too/young"};            target.setPathArray(path);            target.setDescription("也能够在这里批改原有路由的参数信息");            return target;        }        return routeItem;    }});

解析跳转的路由表

导航器跟路由表的交互,最外围的办法就是这个match办法,他是负责将一个url转换成TheRouter路由项的次要办法。

@Synchronizedfun matchRouteMap(url: String?): RouteItem? {    var path = TheRouter.build(url ?: "").simpleUrl    if (path.endsWith("/")) {        path = path.substring(0, path.length - 1)    }    // copy是为了避免内部批改影响路由表    val routeItem = ROUTER_MAP[path]?.copy()    // 因为路由表中的path可能是正则path,要用入参替换掉    routeItem?.path = path    return routeItem}

最初的跳转

最终的跳转,实质上还是调用的 context.startActivity 去做的,所以所有 Activity 的跳转办法,TheRouter也都反对。

if (fragment != null) {    debug("Navigator::navigation", "fragment.startActivity ${routeItem.className}")    fragment.startActivity(intent)} else {    debug("Navigator::navigation", "startActivity ${routeItem.className}")    context.startActivity(intent)}val inAnimId = routeItem.getExtras().getInt(KEY_ANIM_IN)val outAnimId = routeItem.getExtras().getInt(KEY_ANIM_OUT)if (inAnimId != 0 || outAnimId != 0) {    if (context is Activity) {        debug("Navigator::navigation", "overridePendingTransition ${routeItem.className}")        context.overridePendingTransition(            routeItem.getExtras().getInt(KEY_ANIM_IN),            routeItem.getExtras().getInt(KEY_ANIM_OUT)        )    } else {        if (TheRouter.isDebug) {            throw RuntimeException("TheRouter::Navigator context is not Activity, ignore animation")        }    }}

跳转的回调

如果应用TheRouter跳转,传入了一个不辨认的的path,则不会有任何解决。你也能够定义一个默认的全局回调,来解决跳转状况,如果落地页是 Fragment 则不会回调。
当然,跳转后果的回调不止这一个用处,能够依据业务有本人的解决。

回调也能够独自为某一次跳转设置,navigation()办法有重载能够传入设置。

NavigatorKt.defaultNavigationCallback(new NavigationCallback() {    // 落地页Activity关上后,执行到onCreate会回调    @Override    public void onActivityCreated(@NonNull Navigator navigator, @NonNull Activity activity) {        super.onActivityCreated(navigator, activity);    }    // startActivity执行后会立即回调    @Override    public void onArrival(@NonNull Navigator navigator) {        super.onArrival(navigator);    }        // 找到待跳转的落地页时就会回调(startActivity之前)    @Override    public void onFound(@NonNull Navigator navigator) {        super.onFound(navigator);    }    // 找不到落地页的时候会回调    @Override    public void onLost(@NonNull Navigator navigator) {        super.onLost(navigator);    }});

页面参数解析

TheRouter的所有页面跳转参数解析都能够通过 @Autowired 解析的,当然也能通过 Intent 去解析。 Intent解析咱们都会,就不说了,上面讲一下 @Autowired 的实现。

所有加了 @Autowired 注解的类,在编译当前都会生成一个独自的工具类,XXX__TheRouter__Autowired,这个类就是用来填充变量内容的。 实际上咱们调用的inject() 办法:

TheRouter.inject(this);

就会间接调用生成类去做填充变量。

而这个填充过程的实现,实际上是由AutowiredParser去实现的。

这个 Parser 是容许咱们自定义的,也就是说如果咱们心愿替换掉TheRouter的解析,也能够通过自定义的形式实现对 @Autowired 的解析。TheRouter内默认的解析形式是这样的:

class DefaultUrlParser : AutowiredParser {    override fun <T> parse(type: String?, target: Any?, item: AutowiredItem?): T? {        if (item?.id != 0) {            return null        }        when (target) {            is Activity -> {                return parseValue(target.intent?.extras?.get(item.key), type) as T?            }            is Fragment -> {                return parseValue(target.arguments?.get(item.key), type) as T?            }            is androidx.fragment.app.Fragment -> {                return parseValue(target.arguments?.get(item.key), type) as T?            }        }        return null    }    private fun <T> parseValue(value: Any?, type: String?): T? {        if (value == null || type == null) {            return null        }        if (value.javaClass.name.equals(transformNumber(type), true)) {            return value as T?        }        if (value.javaClass.name.equals("java.lang.String")) {            try {                return transform(type, value.toString()) as T?            } catch (e: NumberFormatException) {            }        }        return null    }}

其余API

判断一个 url 是否为路由Path
如果返回为空,示意以后url不是路由表内的path

// kotlin toplevel办法,Java调用请应用RouteMapKt类matchRouteMap("url填这里") == null