前言
组件化页面路由算是个老生常谈的话题了,从蘑菇街的注册 url 到 Casa 的 target-action 方案,市面上也有了很多的优化方案。之所以出这篇文章主要是因为已有方案中的硬编码操作一直不被团队接受,所以一直在寻找更合理的方案,直到不久前从前前同事那得到的思路:注册 protocol 与 viewcontroller 的对应关系,根据 protocol 获取 VC 并调用协议方法。再加上在前公司做过的注册 url 方案,所以打算出篇文章,结合这两种思路总结一套适合当前工程的可行性路由方案。
需求
之所以文章标题叫做:页面跳转 及 路由 方案,主要是对应组件化工程中常见的两大需求:
1、组件间页面跳转解耦:PodA 中的 VC1 想要跳转到 PodB 中的 VC2,但两个组件又不能相互依赖。
2、后台可配的页面路由:页面存在跳转入口,根据后台配置不同的 url 跳往不同的落地页。
应对第一种需求,只要实现简单的解耦即可,正如前言里提到的,通过注册 protocol 与 viewcontroller 的键值对,根据 protocol 获取 VC 并调用协议方法然后 push 即可实现,剩下的就是怎么更合理的注册与取值的问题了。
应对第二种需求,其实可以通过第一种需求的解决方案来实现,只不过那需要大量的 if-else 来判断,并且需要一个中心化的转发类,不管从写法上还是可维护性上都不是最佳方案。
参照上一种解决方案,同样可以提前注册 url 与 viewcontroller 的键值对,根据 url 获取 VC 然后 push,至于传参,将 url 中的参数取出作为 NSDicitionary 传递给 VC 即可,具体实现下面会讲。相比于 if-else 的判断,这种注册 url 的方式好处是:某一 url 的路由支持交给各个组件自己支持,参数的接收也有目标 VC 来控制。
以上,总结出本方案的中心规律:谁提供、谁维护。意思是说,VC 所属于哪个 Pod,那这个 Pod 就有义务为这个 VC 支持 protocol 跳转和 url 跳转,也就是负责注册操作。如果这个 Pod 没有为 VC 进行注册操作,也就没打算将此 VC 对外提供。
预览
期望实现的用法如下:
1 | // 1、protocol 方式支持页面跳转 |
实现
HoloTarget
主要在 HoloTarget
这个核心类来实现,分别为 protocol 和 url 提供了注册和匹配接口:
1 | /// 根据 protocol 注册 target 类 |
.m
的实现很简单, HoloTarget
单例维护了一个 <NSString *, Class>
类型的字典而已,核心代码很简单,大部分代码都是做安全判断的,就不贴代码了,细节在 Demo(HoloTarget.m) 里看吧。
特殊注意的一点是,注册和匹配 url 的时候,是通过 NSString+HoloTargetUrlParser 这个分类获取 path
进行注册的。
HoloNavigator
HoloNavigator
这个类是对 HoloTarget
的封装,提供了 macth viewcontroller 的功能:
1 | /// 根据 protocol 匹配 viewController 实例 |
其中在 matchViewControllerWithUrl:
方法里做了 scheme
判断,如果是 http
或者 https
的话,判断代理是否返回 web viewcontroller
。
并且 HoloTarget
设置过 businessScheme
的话,对 url scheme
加了强校验,只有符合 business scheme
时才继续匹配:
1 | + (nullable UIViewController *)matchViewControllerWithUrl:(NSString *)url { |
使用姿势
1、 protocol pool
创建 protocol pool 组件,被所有提供方和调用方依赖,或者将 HoloTarget 集成进当前组件化工程,创建 protocol pool 文件夹管理所有的 protocol。
2、url 入参取值
通过 url 匹配到 vc 的原理是获取 url 里的 path 进行匹配,获取到 vc 后,会将 url 里的 params 与 vc 绑定存储起来:
1 | + (nullable UIViewController *)matchViewControllerWithUrl:(NSString *)url { |
所以可以在 vc 的 viewDidLoad 方法内获取 params 做入参处理:
1 | - (void)viewDidLoad { |
3、Target for Pod
如果一个 protocol 对应一个 viewcontroller,而当前 Pod 对外提供的 viewcontroller 又比较多的话,可以为 Pod 创建一个 MainTarget 类,进行 MainTarget - MainTargetProtocol 一次注册,在 MainTarget 类里转发对组件内各个 vc 的调用,如:
1 | // 1、提供 MainTarget 类 |
4、HoloLifecycle 提供注册时机
对 protocol 和 url 注册操作应该在哪里?这么做?根据 谁提供、谁维护 的原则,应当由各个 Pod 自己负责自己 vc 的注册操作。之前发过的一篇文章 组件化分发生命周期 介绍了 HoloLifecycle 这个工具,正好满足了当下的需求。
为每个 Pod 创建 Lifecycle 类,将主工程 AppDelegate 的生命周期分发给各个 Pod,在 Lifecycle 类里完成 target 的注册操作(protocol & url)。
5、YAML 提供注册时机
除了通过 HoloLifecycle 手动注册,HoloTarget 还提供了通过 YAML 配置文件自动注册的操作,在 Build Phases 添加 holo_target_generator.rb
脚本的执行(注意脚本路径改成脚本最终所在的位置):
该脚本的目的是收集各个 Pod 内的 holo_target.yaml 配置文件。脚本内容:
1 |
|
调用 HoloTarget.h
类的 registAllTargetsFromYAML
方法进行收集注册,具体实现参考 HoloTarget.m#L28-L80
1 | - (void)registAllTargetsFromYAML { |
然后在各个 Pod 内部创建 holo_target.yaml
配置文件。
注意1:配置文件名称必须是 holo_target.yaml
。
注意2:组件内的 holo_target.yaml
配置文件不要放在 Main Bundle 内,会被互相覆盖。
holo_target.yaml
配置文件示例:
1 | # Requirement: name your file holo_target.yaml |
选择 YAML 格式的主要原因是因为该格式支持添加注释,而且写法也比 JSON 简洁很多,是很多配置文件的首选格式,注意 YAML 使用缩进表示层级关系,参考:YAML 语言教程
安全防护
通过 protocol 获取 vc 并调用方法时,如:
1 | UIViewController *vc = [HoloNavigator matchViewControllerWithProtocol:@protocol(ViewControllerProtocol)]; |
如果该 vc 未实现 viewController:
方法的话程序就会崩溃,虽然 HoloTarget 在注册时做了 taeget 是否遵守 protocol 的判断,如:
1 | - (BOOL)registTarget:(Class)target withProtocol:(Protocol *)protocol { |
但正常经常下,vc 如果遵守了协议但未实现方法的话也只有个警告而已。当然,你可以通过加 respondsToSelector:
判断这种最简单的方式解决这个 crash 隐患:
1 | if ([vc respondsToSelector:@selector(viewController:)]) { |
HoloTarget 为了省掉这个 if 判断,最开始创建了 NSObject+HoloTargetUnrecognizedSelector
分类,想要防护所有类的 unrecognized selector
崩溃:
1 | @implementation HoloTargetStubProxy |
正如注释所写的,暂时没办法获取到 forwardInvocation:
方法是否被交换过,如果直接在 forwardingTargetForSelector:
方法里转发的话肯定会有问题。比如被 Aspects hook 过的方法,其实是将被 hook 的对象 isa 指向了动态创建的类,被 hook 的方法执行时进入消息转发流程,并且 Aspects 交换了 forwardInvocation:
方法在新的 forwardInvocation:
方法里执行了被 hook 方法。
所以如果在以上 _holo_forwardingTargetForSelector:
方法里无脑转发给 [HoloTargetStubProxy new]
对象的话那被 Aspects hook 的方法就是失效了。如果有方法能够判断某个方法是否被交换过就好了,这样就可以做判断:如果当前类及父类重写或交换了 resolveInstanceMethod:
方法、forwardInvocation:
方法,直接 return nil
。
如果你有方法能够判断某个方法是否被交换过,请告知我,谢谢!
以上方案走不通的话,那可以在工程中添加 OCLint 规则,判断当前类遵守了协议但未实现协议方法的话直接报错,编译失败。
当前 HoloTarget 也提供了备选方案,创建了 HoloBaseTarget
、HoloBaseTargetViewController
分别继承了 NSObject
、UIViewController
的两个类,在这两个类的里重写了 resolveInstanceMethod:
方法,做了安全防护:
1 | + (BOOL)resolveInstanceMethod:(SEL)sel { |
让将要注册的 target 继承这两个类就可以了。
以上,就是 HoloTarget 的所有内容了
项目地址:https://github.com/HoloFoundation/HoloTarget
isa-swizzling (更新于 2020.7.18)
之前也考虑过这种方案,参考 KVO
的实现原理,通过 isa-swizzling
技术,动态创建一个新的类,将目标对象的 isa
指针指向这个新的类,对新的类做安全保护。但因为之前的思路一直局限于通过 isa-swizzling
保护对象,因为 HoloTarget
还提供了获取 Class
接口,所以一开始并没有选用这种方案。(HoloTarget
提供了 match Target Class
的接口,通过获取 Class
可以调用 init
的方法)
现在看来的确是之前的思路狭隘了,包括前文提到的:判断 resolveInstanceMethod:
方法、forwardInvocation:
方法等是否被处理过,其实并不需要,我们只需要关心 forwardInvocation:
方法即可,因为如果消息转发的前几步就被特殊处理过了,就不会走到 forwardInvocation:
方法了,而被特殊处理过的对象及类,HoloTarget
就不再关心了。
所以,最终的解决方案是:
1、保护对象:
1.1、根据目标对象的类动态创建一个新的子类
1.2、将目标对象的 isa
指针指向这个新的类
1.3、交换这个新的类的 forwardInvocation:
方法为自己的 holo_forwardInvocation
方法,只要这个自己的方法不再继续向下走消息转发流程,就不会崩溃了。
(注意要保证方法只被交互一次)
2、保护类:
将类的 forwardInvocation:
方法交换为自己的 holo_forwardInvocation
方法
(注意要保证方法只被交互一次)
方便的是,以上步骤就是 Aspects 里的一部分逻辑,直接将这部分逻辑单独拷贝出来应用即可:
1 | static NSString *const HoloTargetSubclassSuffix = @"_holoTarget_"; |
其实 HoloTarget
里只保护了类,因为 matchTargetInstance
方法也是直接拿到的类执行了 new
方法。
因为加了 holo_methodHasOverwrited
方法判断,类的 forwardInvocation:
方法是否被交换过也提前判断了。