RxSwift之中介者模式
定义: 用一个中介对象(中介者)来封装一些列的对象交互,中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变他们之间的交互。
从上面 中介者模式 的定义似乎知道了中介者的作用,但是具体如何使用呢?那么下面我将和小伙伴们一起来实现一个中介者。
在项目开发中,很多时候都会用到定时器。我们可能会写出如下代码:
class ViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.init(timeInterval: 1, target: self, selector: #selector(timeFire), userInfo: nil, repeats: true)
RunLoop.current.add(timer!, forMode: .common)
}
@objc func timeFire() {
print("time fire")
}
deinit {
timer?.invalidate()
timer = nil
print("控制器销毁了")
}
}
执行代码,定时器开始走了。但是当我们离开控制器时,就会发现,deinit
并没有执行。因为我们给 Timer
的 target
传入的是 self
,所以会造成循环引用。
要想解决这个问题,我们可以使用block方式初始化 Timer,如下:
timer = Timer.init(timeInterval: 1, repeats: true
, block: { (timer) in
print("timer fire")
})
这样就解决了循环引用的问题,但是我们会发现block方式初始化 Timer 这个方法是在 iOS10 以后才出现的,所以在低于 iOS10 的版本上是无法使用的,即使没有这样的问题,但是每次都要重写 deinit
这个方法来停止定时器也是一个不够优雅的方式。
是否可以有一个其他东西来帮我解决定时器造成的循环引用以及停止定时器呢?这就是我们即将介绍的 中介者模式。
既然是 中介者模式,那么必然会有一个 中介者, 所以我们新建一个继承自 NSObject 的类 BOProxy 来充当 中介者。
class BOProxy: NSObject {
weak var target: NSObjectProtocol?
fileprivate var sel: Selector?
fileprivate var timer: Timer?
override init() {
super.init()
}
}
因为中介者也需要知道外界的信息,所以需要保存响应者 target
,又为了打破循环引用,所以使用 weak 修饰符。使用 sel
保存 timer 调用的方法。使用 timer
保存内部初始化的定时器。
我们再增加一个 sechduleTimer
函数来初始化定时器,接收外界传入的参数。
func sechduleTimer(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool) {
timer = Timer.init(timeInterval: ti, target: aTarget, selector: aSelector, userInfo: userInfo, repeats: yesOrNo)
self.target = aTarget as? NSObjectProtocol
self.sel = aSelector
guard target?.responds(to: sel) == true else {
return
}
RunLoop.current.add(timer!, forMode: .common)
}
同时修改 controller 中的代码:
proxy.sechduleTimer(timeInterval: 1, target: self, selector: #selector(timeFire), userInfo: nil, repeats: true)
然后执行,定时器能够响应。但是,退成控制器后,定时器并没有被销毁啊。而且循环引用仍然存在,是中介者没有起作用吗?不是的。
我们 sechduleTimer
中初始化 Timer
的时候,直接使用传入 target
来初始化 Timer
,所以循环引用并没有被打破,中介者在其中仅充当封装者的作用,并没有起到其应有的作用。
所以,我们应该使用 BOProxy 自身,来初始化 Timer:
timer = Timer.init(timeInterval: ti, target: self, selector: aSelector, userInfo: userInfo, repeats: yesOrNo)
让 BOProxy来响应定时器的回调,但是又会出现一个问题,BOProxy 并没有实现定时器回调的 selector
啊。为了让 BOProxy 拥有外面的 seletor
,我们使用 runtime 做方法交换。
...
RunLoop.current.add(timer!, forMode: .common)
let method = class_getInstanceMethod(self.classForCoder, #selector(boTimeFire))!
class_replaceMethod(self.classForCoder, self.sel!, method_getImplementation(method), method_getTypeEncoding(method))
最终,定时器就会回调 BOProxy.boTimeFire 函数。我们在 boTimeFire
函数中再来让 target
调用 selector
。
@objc fileprivate func boTimeFire() {
if self.target != nil {
self.target!.perform(self.sel)
} else {
self.timer?.invalidate()
self.timer = nil
}
}
同时,还判断外界的 target
是否还存在,如果不存在则销毁定时器。完成定时器的自动销毁。
执行上面的代码,定时器完美响应,退出控制器,控制器也销毁了。
如果是初级开发者,做到这一步,已经可以了。但毕竟我是比初级开发者高一点点的初级开发者,那么我可能在传入 BOProxy selector
的时候,这样写:
let sel = NSSelectorFromString("timeFireNoIMP")
proxy.sechduleTimer(timeInterval: 1, target: self, selector: sel, userInfo: nil, repeats: true)
但是呢,timeFireNoIMP
这个方法并没有被实现,那么再运行会怎么样呢?我们会发现,没有报错,但是定时器也没有响应。
这是因为我们在 sechduleTimer
中 判断了 target?.responds(to: sel)
必须要 target
实现了 sel
才把 Timer 加入到RunLoop中。但是这样并不好,因为你找不到错误的原因,不容易定位错误。
我们可以在判断到 target
未实现 sel
时,打印一个提示。
guard target?.responds(to: sel) == true else {
print("\(sel!) 方法未实现")
return
}
但是,如果项目中log太多的话,可能并不好发现这个提示。那么还有另一种方式,我一直认为,如果有bug,在开发/测试阶段最好是能直接暴露出来,而不是等APP发布之后才被发现。没有比崩溃更能暴露bug的了。
所以,我们直接将 timer
加入RunLoop,不做 selector
是否实现的判断。那么,在 selector
未被实现的情况下,必然会导致APP崩溃,但是在崩溃之前因为iOS的容错机制,其必然会进入消息转发阶段。我们就在消息转发中来做错误提示,便于定位bug所在。
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if self.target?.responds(to: sel) == true {
return self.target
} else {
print("\(sel!) 方法未实现")
return super.forwardingTarget(for: sel)
}
}
这里我只做了简单的处理,感兴趣的小伙伴可以继续拓展,比如为了避免崩溃,可以在消息转发阶段使用 class_addMethod
方法加入自己实现的容错方法。
以上就是 中介者模式 的一个简单示例。而RxSwift中大量使用了 中介者,比如 Sink,来处理一些不方便暴露的方法,以及解耦各个对象之间的联系。若有不足之处,请评论指正。