今天在 qq 上看到有人發了一段代碼,在 iOS 8 裏按 button 會閃退,在 iOS 9 以上的版本就能夠正常運行。html
class ViewController: UIViewController {
dynamic func click() { ... }
let button: UIButton = {
let button = UIButton()
button.addTarget(self,
action: #selector(click),
for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(button)
}
... other code ...
}複製代碼
第一眼的感受是這段代碼寫得頗有問題,不該該在 button 初始化的時候 addTarget
,由於這個時候 self 尚未初始化完成,或者應該使用 lazy var,但仍是不理解爲何 iOS 9 以上的版本就不會,報錯信息是這樣子的:git
-[__NSCFString tap]: unrecognized selector sent to instance 0x7fac00d0bf40github
一看就感受是 addTarget
調用的時候 self
還沒初始化完成,指向了內存裏任意一段數據。swift
首先我懷疑是初始化的順序出了問題,會不會由於在 iOS 8 裏,編譯器自動生成的 init 方法內部實現有問題,相似於這樣:閉包
init(coder aDecoder: NSCoder) {
button = { ... }()
super.init(coder: aDecoder)
}複製代碼
在 self
初始化以前,button
就提早訪問了 self
,而後在 iOS 9 以後是爲了這方面兼容性的考慮,在自動生成的 init 方法裏,先調用 super.init
,再初始化屬性。架構
一開始以爲可能大概就是這樣,後面越想越不對,寫了段代碼去驗證本身的想法:ide
class FatherVC: UIViewController {
init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
print("FatherVC")
}
}
class ChildVC: FatherVC {
var button: UIButton = {
var button = UIButton
... set up ...
print("button initialized")
return button
}()
... other code ...
}複製代碼
在任意版本的系統上,先打印出的是 "button initialized",super.init
最後才調用的,初始化的順序的猜測是錯誤的。測試
想了好久都沒有思路,就試着在 iOS 8,9,10 裏把這幾個相關的屬性打印了出來,都是如出一轍的結果:ui
button.target(forAction: #selector(click), withSender: nil)
// ViewController
button.allTargets
// null
self
// (ViewController) -> () -> Viewcontroller
// 在 button 初始化的 block 裏複製代碼
能夠確定貓膩就在 addTarget
方法裏,由於 input 都是同樣的。spa
這裏最奇怪的地方是 self
是一個 block,但根本沒有方法經過這個 block 去獲取初始化以後的對象。我想了好幾種可能性,後面甚至把 addTarget 的第一個參數換成了相同類型的空閉包,發現居然還能夠正常運行,接着又再試着傳入各類值,例如 Int
,String
,() -> Int
,均可以正常運行(iOS 9)。
這個時候就又卡住了,只好去翻文檔看看有沒有什麼線索,看到這麼一段話:
The target object—that is, the object whose action method is called. If you specify nil, UIKit searches the responder chain for an object that responds to the specified action message and delivers the message to that object.
忽然在想,會不會是 addTarget
方法會先判斷一下 target
是否爲 block?若是是 block 的話,就當作是 nil,事件觸發時沿着 responder chain 去找,若是可以響應 click
的話,就調用,這樣的話 button.allTargets
爲 null 也就說得通了。寫代碼測試:
class CustomView: UIView {
func responds(to aSelector: Selector!) -> Bool {
print(aSelector)
return super.responds(to: aSelector)
}
}
class ViewController: UIViewController {
... other code ...
override func viewDidLoad() {
super.viewDidLoad()
customView.addSubview(button)
view.addSubview(customView)
}
}複製代碼
在 button
和 ViewController
這條響應鏈中間再插入一個 responder 去攔截消息,只要有打印出 click
方法,就表明着確實是順着響應鏈尋找 responder。運行以後確實打印出了 click
方法,猜測正確。
以後我又給 addTarget 傳入了好幾種值,最後發現具體的實現應該是相似於這樣的:
// iOS 8
func addTarget(_ target: Any?, action: Selector, for event: UIControlEvent) {
if let objectCanRespond = target {
// 在 event 觸發以後,直接給 target 發送一個 action 消息
} else {
// 在 event 觸發以後,順着響應鏈尋找可以響應 action 的對象
}
}
// iOS 9 以上
func addTarget(_ target: Any?, action: Selector, for event: UIControlEvent) {
if let objectCanRespond = target as? NSObject { ... }
else { ... }
}複製代碼
理清了這個問題以後,我開始以爲其實這種直接順着響應鏈尋找 responder 的作法也不錯,寫 Swift 常常會遇到這種狀況:
class ViewController: UIViewController {
// 1.
let button: UIButton = ...
override func viewDidLoad() {
...
button.addTarget(self,
action: #selector(click),
for: .touchUpInside)
}
// 2.
let button: UIButton
override init() {
button = ...
super.init()
button.addTarget(self,
action: #selector(click),
for: .touchUpInside)
}
// 3.
lazy var button: UIButton = {
...
button.addTarget(nil,
action: #selector(click),
for: .touchUpInside)
return button
}()
}複製代碼
第一和第二種寫法會讓 button
的配置代碼變得分散,在初始化的時候配置樣式,以後再 addTarget
;而第三種寫法則會必須使用 var 去聲明 button
,但咱們根本不但願 button
是 mutable 的。
而直接給 addTarget 傳入 nil 的話,讓 action 順着響應鏈去尋找 responder 的話,就沒有必要在 button 初始化時明確 responder,有一篇文章專門寫如何經過響應鏈機制進行解耦,推薦你們能夠看。
這樣代碼能夠組織得更好,並且也是一種合理的抽象。惟一的缺點就是 target 必須處於響應鏈上,使用 MVVM 之類的架構可能會有侷限。
以爲文章還不錯的話能夠關注一下個人博客