定義: 用一箇中介對象(中介者)來封裝一些列的對象交互,中介者使各對象不須要顯示地相互引用,從而使其耦合鬆散,並且能夠獨立地改變他們之間的交互。swift
從上面 中介者模式 的定義彷佛知道了中介者的做用,可是具體如何使用呢?那麼下面我將和小夥伴們一塊兒來實現一箇中介者。bash
在項目開發中,不少時候都會用到定時器。咱們可能會寫出以下代碼:ide
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,以下:oop
timer = Timer.init(timeInterval: 1, repeats: true
, block: { (timer) in
print("timer fire")
})
複製代碼
這樣就解決了循環引用的問題,可是咱們會發現block方式初始化 Timer 這個方法是在 iOS10 之後纔出現的,因此在低於 iOS10 的版本上是沒法使用的,即便沒有這樣的問題,可是每次都要重寫 deinit
這個方法來中止定時器也是一個不夠優雅的方式。測試
是否能夠有一個其餘東西來幫我解決定時器形成的循環引用以及中止定時器呢?這就是咱們即將介紹的 中介者模式。ui
既然是 中介者模式,那麼必然會有一個 中介者, 因此咱們新建一個繼承自 NSObject 的類 BOProxy 來充當 中介者。spa
class BOProxy: NSObject {
weak var target: NSObjectProtocol?
fileprivate var sel: Selector?
fileprivate var timer: Timer?
override init() {
super.init()
}
}
複製代碼
由於中介者也須要知道外界的信息,因此須要保存響應者 target
,又爲了打破循環引用,因此使用 weak 修飾符。使用 sel
保存 timer 調用的方法。使用 timer
保存內部初始化的定時器。code
咱們再增長一個 sechduleTimer
函數來初始化定時器,接收外界傳入的參數。orm
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,來處理一些不方便暴露的方法,以及解耦各個對象之間的聯繫。如有不足之處,請評論指正。