TheRouter 页面跳转源码分析_一只小菜鸡^
TheRouter是什么
TheRouter是货拉拉开源项目的 Android app中对新页面、服务保障模块图片化推进开发管理、具备路由职能的中间商件,倡导的是非常简单且能用。Github: //github.com/HuolalaTech/hll-wp-therouter-android
官网: //therouter.cn
简单使用示例
Activtiy跳转
TheRouter.build("//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("//therouter.cn/test/activity").withString("name", "姓名").navigation();
从最喜欢用的加载起讲解,基本的可介绍到 TheRouter 的旋转道理。这一行成功加载的源代码后面成果是过飞机安检数据加载到相对的 Activity,在 Android 这方面再说后必要是借助加载 startActivity 和 startActivityForResult 来成功加载。
分类一步看:
- TheRouter 调用 Build 生成 Navigator,过程是怎样的Navigator 是什么Navigator 调用 navigation 是怎样执行到 startActivity 的
生成 Navigator
通过查看源码TheRouter.build()
实际上调用的是Navigator
的构造方法,
@JvmStatic
fun 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 是什么
根据 官网文档的介绍 //therouter.cn/docs/2022/08/28/01
TheRouter的页面跳转都由 Navigator 导航器去操作的。
参数的解析
我看的是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=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)
}
方面被注音掉的编码只是 老的版本的详细分析,注音写的很知道,仍然uri.queryParameterNames() 会会自动decode,从而造成表面道理误区,但是新的版本就调成了只能根据&自功抓取k=v的形式有了。
在执行解析uri之前,其实还做了一次拦截器的替换。
按照官网文档:Path 修改器的应用场景是用于修复客户端上路由 path 错误问题。
for (handle in fixHandles) {
handle?.let {
url = it.fix(url)
}
}
路由表
TheRouter是个动态路由,所以路由表其实被弱化为一个很泛的概念了。
感觉有很多套路由表,最终都会被汇总到一个支持正则表达式的Map里面。
APT生成的路由表
看一是种,非常好表达的,都是注解解决器分析转化的。@Route(path = "//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\":\"//kymjs.com/business_a/testinject\",\"className\":
\"com.therouter.demo.shell.TestInjectActivity\",\"action\":\"\",\"description\":\"\",\"params\":{}},{\"path\":\"//kymjs.com/business_a/testinject3\",
\"className\":\"com.therouter.demo.shell.TestActivity\",\"action\":\"\",
\"description\":\"\",\"params\":{}},{\"path\":\"//kymjs.com/business_a/testinject4\",\"className\":\"com.therouter.demo.shell.MultiThreadActivity\",
\"action\":\"\",\"description\":\"\",\"params\":{}},{\"path\":\"//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("//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("//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("//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("//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打包后自动将assets/目录中的配置文件上传到配置系统,下发给对应版本APP 。优点在于全自动不会出错。配置系统无法打通,线上手动下发需要修改的路由项,因为 TheRouter 会自动用最新下发的路由项覆盖包内的路由项。优点在于精确,且流量资源占用小。
// 此代码 必须 在 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 的跳转。
延迟跳转
延缓调用是个很革新的来设计,装有官站的讲法,延缓调用注意应该用行业应用有两类: 第一种:初始化时期,如果路由表的量非常巨大时。这种情况在别的路由框架上要么会白屏一段时间,要么直接丢弃这次跳转。在TheRouter中,框架会暂存当前的跳转动作,在路由表初始化完成后立刻执行跳转。第二种:从Android 8.0开始,Activity 不能在后台启动页面,这对于业务判断造成了很大的影响。由于可能会有前台 Service 的情况,不能单纯以 Activity 生命周期判断前后台。在TheRouter中,框架允许业务自定义前后台规则,如果为后台情况,可以将跳转动作暂存,当进入前台后再恢复跳转。// 暂存的动作可以有多个,会在恢复时按顺序执行
TheRouter.build("//therouter.com/home")
.withInt("key1", 12345678)
.padding()// 暂存当前跳转动作
.navigation(context);
// 恢复
//toplevel方法,无需类名调用,Java请通过NavigatorKt类名调用
sendPendingNavigator();
又是拦截器
在这一步其实是有两个拦截器,一个是在路由表解析之前,一个是在路由表解析之后。
这里我就一起讲了。
- 路由表解析之前的,叫 页面替换器
例如模块化的时候,皇冠新体育APP壳模板组件中开发了一个SplashActivity
广告组件作为应用的MainActivity,在闪屏广告结束的时候自动跳转业务皇冠新体育APP页面。 但是每个业务不同,皇冠新体育APP页面的 Path 也不相同,而不希望让每个业务线自己去改这个皇冠新体育APP壳模板组件,此时就可以组件中先写占位符//kymjs.com/splash/to/home
,让接入方通过 Path 替换器解决。
Navigator.addPathReplaceInterceptor(new PathReplaceInterceptor() {
@Override
public String replace(String path) {
if ("//kymjs.com/splash/to/home".equals(path)) {
return "//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 = {"//kymjs.com/too/young"};
target.setPathArray(path);
target.setDescription("也可以在这里修改原有路由的参数信息");
return target;
}
return routeItem;
}
});
解析跳转的路由表
导航系统器跟路由表的互动,最体系化的办法也就是整个match办法,他是主管将一款url转型成TheRouter路由项的重要办法。@Synchronized
fun 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 则不会回调。
当然,跳转结果的回调不止这一个用途,可以根据业务有自己的处理。
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
皇冠新体育APP相关的文章
- 前端一面react面试题解析整理_m0_57781768
- FastDFS简介/架构/安装/代码_我的搬砖日常
- LeetCode(String) 2114. Maximum Number of Words Found in Sentences_Smile sea breeze
- 皇冠新体育APP:yarn包管理器_悠然予夏
- 一文读懂JVM类加载机制过程及原理万字详解_JermeryBesian
- 关于TCP和UDP的认识以及一些常用概念_ivy_0709
- 皇冠新体育APP:大数取余公式_GitNoob
- Numpy数据分析csv文件的应用_恶霸程序员388
- find it/ something + adj_weixin_40248634
- [ 数据结构 ] 二叉排序树(BST)_bone_ds
- 皇冠新体育APP:Java学习(41)类和对象以及类的创建_炎武丶航
- Spring4 全细节回顾_pshdhx_albert
- 皇冠新体育APP:【Kotlin】空安全 ⑤ ( 异常处理 | 捕获并处理异常 | 抛出自定义异常 )_韩曙亮
- 皇冠新体育APP:果蔬消毒机行业市场深度监测及发展趋势预测分析_a18811791343
- 启动ruoyi框架(初学)_cai-4
- 皇冠新体育APP:数据分析-深度学习 Day3_小浩码出未来!