先说下背景:
我们想通过一种统一的跳转链接,来实现H5、原生及端外跳转到端内等几种场景下的统一跳转。这就涉及到页面跳转组件。这些跳转组件的设计比较考验开发人员的能力,如何做才能减少页面间的耦合,提高模块化呢?本文将为你讲述一种实现思路,至于优劣,见仁见智。
首先。我们的跳转链接大致如下:
qimi://juanpi?type=1&content={"a":"ssssss","b":"gggg"}
这里面type是跳转页面的类型,content是需要的参数,用json形式组装,由各个页面自己解析。
一、原始想法
最开始我的想法是这样的:
首先,所有跳转都得经过一个类,这个类里面得包含所有需要跳转的页面,然后根据参数来区分具体跳转到哪里。这个类我们暂定为QMAction,核心的方法大概是这样的:
-(void)handleJumpWithType:(NSInteger)type withParams:(NSDictionary:)params jumpController:(UINavigationController*)controller {
UIViewController *destVC;
if (type == 1) //首页 {
destVC = [[HomeViewController alloc] init];
} else if(type == 2) //列表页面 {
destVC = [[ListViewController alloc] init];
}
.........//还有很多类似的判断
if(destVC) {
[controller pushViewController:destVC];
}
}
但是这么做有几个问题:
1. 首先,跳转的来源包括H5页面、外部app、推送或者任何本地页面,有的情况下需要push一个新页面,有的情况需要先pop回首页再push,如何适应多样化的需求?
2. 每次新增type,就要多引用一个头文件,即引用依赖过多。
3. 创建VC的代码大部分都是重复的,这种重复的alloc init 实际上是在扼杀编程的热情。我们需要减少重复代码。
基于这几个问题,我们尝试重新设计页面跳转组件。
二、解决引用依赖
熟悉OC的朋友都知道,OC有一个动态运行机制,所有的类和方法都可以通过字符串来获得,比如NSSelectorFromString,NSClassFromString。如果我用这个方法,让外部把类名传递进来,是不是就不用那么多头文件了呢?而且这样做if...else if...else 这种判断也大大减少了。
抱着这个想法,我尝试了修改handleJumpWithType:params:jumpController:这个方法,确实是可以动态的去获取类,但是有一个问题,有的VC类名特别长,而且在推送和H5页面跳转的场景下,得针对安卓和ios做区分,因为两边的类名并不相同。于是,这个方案很快就被否决了。那么,还有没有其它的方式呢?
可不可以在QMAction内部做一个映射呢?通过一个字典奖type和类名对应起来,这样后端只需要知道某个页面对应的type,就可以设置跳转链接了。这个方法有一定的灵活性,但还是面临一个问题,就是当类名改了以后,这个映射的字典里面也得做对应的修改,如果开发人员忘记改了,就跳转不了了。这种方式维护起来还是不够简单,而且还是需要引用头文件。
那么,能不能用插件的方式呢?从之前了解过的一些浏览器插件管理器的实现方式来看,我们可以将每一个页面当作一个插件,通过轮询的方式来得到哪一个插件能响应我需要调用的方法。他的代码大概是这样的:
for (Plugin * plugin in self.plugins) {
if([plugin respondToSelector:NSSelectorFromString(xxxx)]) {
[plugin performSelector:NSSelectorFromString(xxxx)];
break;
}
}
这里的前提是,每一个插件都继承自Plugin的基类。而self.plugins数组里面包含的就是所有的插件,这些插件是在程序启动的时候通过代码或者plist文件添加到这个数组里面的。
我们把每一个页面当做一个插件来处理,在QMActon中只需要引入一个Plugin基类,再在各个类中加一个方法,判断能不能处理跳转的请求,这样不就大大减少了引用的文件了吗?
三、协议与遍历
通过第二步的改良,其实我们已经算是解决了引用依赖过多的问题,但是这样做代码的侵入性还是有点高,因为每个VC都得继承自同一个基类,这对于之前没有共同基类的VC来说改动还是有点大,那么能不能在不改变原来类的继承关系的条件下来实现呢?
这时候我想到了协议(或者说代理)。
协议是目前所有重构方案里面代码侵入性最小的,一不需要改变基类,对协议的宿主(也就是实现协议的类)没有类型上的要求,方便移植;二不强制要求宿主类必须实现某个方法(协议方法分为可选和必选两种)。
比如之前那段轮询的代码,用协议实现起来是这样的:
for (id * plugin in self.plugins) {
if([id<QMActionProtocol> respondToSelector:NSSelectorFromString(xxxx)]) {
[id<QMActionProtocol> performSelector:NSSelectorFromString(xxxx)];
break;
}
}
这样,我并不需要引用具体的子类,是不是看起来好一点呢?
但是,还有一个问题,如果我要像H5插件那样在程序启动时把所有插件类注册一遍,未免有些low了,这种引用依赖关系或多或少还是存在一点。有没有更彻底的办法呢?
这时候,就要用到runtime了。
我们知道runtime可以动态判断一个类有没有实现一个方法,那他应该也可以判断一个类有没有实现一个协议。带着疑问,我查阅了一下runtime的文档,找到了这个方法:
OBJC_EXPORT BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
有了这个方法,我就可以不需要自己去写代码一行行注册插件了,只需要先遍历系统的所有类,然后判断每个类是否实现了这个协议就可以了。
遍历获取某个协议的类为了减少每次遍历的性能消耗(类越多,消耗越大),我们可以用一个数组把这些类缓存下来,下次直接从数组里面读出。
四、协议的具体实现
前面只是通过运行时的方法拿到了所有类,但是轮询是避免不了的,我们需要对每一个类做判断,是否对应了我们的type。另外,还需要一个方法来创建VC,以及另一个方法来做一些特殊处理
协议方法每个遵循了QMActionProtocol的类需要实现这些协议方法(1必须实现,2、3可选),其中QMAction里面就含有type和content。 同一个类可以对应多个type,但一个type只能对应一个类,否则系统就会根据遍历的顺序跳到优先跳到第一个对应这个type的类,而这个顺序是随机的。
当所有类都轮询过一遍之后,就有了type和class的对应关系,而这个关系以后会频繁用到,所以我们可以用NSCache(一种Key-Value的容器,类似NSDictionary,但内存不足时会自动释放)缓存下来。
创建VC的那个方法所做的就是解析content,给VC初始化并对相应属性赋值,对于一些简单的VC,只需要一句
id viewcontroller = [[[self class] alloc] init];
就可以了。
参数的赋值可以通过字典和KVC 解决,我们将自定义的一个字典对象当作参数容器传给VC,VC拿到后,调用setValuesForObject 方法,把字典的key当作属性名称,value当作属性值,直接赋值。
KVC赋值其中一个例子:
协议方法例子五、子类化QMAction与默认值
前面说的是如何运用协议和运行时来解决依赖和耦合的问题,现在要解决的另一个问题是如何隐藏不同类之间的差异。
我们知道QMAction是一个跳转组件, 一般情况下需要创建一个VC,然后跳转过去, 但是有些特殊场景是不需要创建VC的,比如说在当前VC上添加一个view,或者分享某个页面到第三方平台,这种看似差不多的功能实际上处理起来很不一样。为了让QMAction能适应所有的场景,就需要统一调用方式,隐藏内部的差异。
我们把QMAction拆成了三类,QMPushAction, QMShareAction和QMCustomAction。三个类分别对应普通的跳转、分享以及自定义的场景(如弹起浮层,关闭某个页面)等。这三个类共同继承自QMAction。同时,在类的内部,我们加上了一段代码指定某个type对应的默认class。这么做的好处是不用在调用的代码里显式申明action的类型,在H5、外部app等非原生代码调用的场景下能自动匹配到对应的类型。
QMAction默认实现同样的技巧我们也用在了QMPushAction的transitionStyle上,这个属性指定了跳转的方式,如Push,Pop,Present等。除非是调用的时候显式的指定了transitionStyle,否则以下这些type会按默认的跳转方式来执行,这在H5页面跳转到原生页面时特别有效。这里transitionStyle的类型用了NSNumber而不是NSInteger 是因为NSNumber默认是nil,可以使用懒加载,而NSInteger不行。
跳转方式默认实现下面是使用transitionStyle的代码:
使用transitionStyle
网友评论