Swift-漏掉lazy引發的一個神奇的bug

語言: CN / TW / HK

theme: github

在上線的當前晚上,測試發現有一個按鈕點選無響應,測試給出的復現路徑是:在release包裡,進入到對應介面,鍵盤彈出按鈕可正常點選,鍵盤收起按鈕無法點選,debug包也可以正常點選。如果你對這個bug感興趣,點選下載最簡化的demo.

其中可復現的核心程式碼如下

```Swift class ViewController: UIViewController { var btn: UIButton = { let btn = UIButton(type: .system) btn.frame = CGRect(x: 100, y: 100, width: 60, height: 40) btn.setTitle("點我", for: .normal) btn.addTarget(self, action: #selector(onTap), for: .touchUpInside) return btn }()

var textField: UITextField = {
    let textField = UITextField()
    textField.placeholder = "請輸入"
    textField.frame = CGRect(x: 100, y: 160, width: 60, height: 40)
    return textField
}()

override func viewDidLoad() {
    super.viewDidLoad()
    self.view.backgroundColor = UIColor.white
    view.addSubview(btn)
    view.addSubview(textField)
}

@objc func onTap() {
    print("onTap")
}

}

extension UIWindow { #if DEBUG #else open override var canBecomeFirstResponder: Bool { return true } #endif } ```

在看下面的解釋之前,你可以先看看上面的程式碼,是否應該能復現出上述的bug?

當然,答案一定是肯定會出現上面的問題。當然你可能會有以下的疑問?

  1. 為什麼debug的時候onTap方法會被呼叫?
  2. 為什麼鍵盤彈起的時候onTap方法會被呼叫?

在上面的程式碼中,btn本意是要寫成懶載入的,結果由於複製失誤,導致複製的時候,少複製了一個lazy,導致btn變成了非懶載入的儲存變數,後面變成了一個立即執行的閉包。在閉包執行的時候ViewController還未進行初始化完成,閉包中的self其實是一個Function, 由於btn的target會嘗試轉為NSObject,這裡一定轉化不成功,我們這裡簡單認定為btn的target等於nil。

也就是說上面的程式碼可以簡單的等同於btn.addTarget(nil, action: #selector(onTap), for: .touchUpInside).

而當對一個按鈕新增事件的時候,如果target為nil,系統會進行一下的操作

  1. 首先獲取APP當前的firstResponder,如果firstResponder為nil,則進行第4步,否則進行下一步
  2. 判斷firstResponder是否實現此target對應的action,如果實現了,則進行呼叫。
  3. 否則則判斷firstResponder的nextResponder是否實現,如此迴圈判斷,一直到nextResponder為nil
  4. 判斷btn是否實現了對應的action,如果實現,則進行呼叫。
  5. 否則則判斷btn的nextResponder是否實現了對應的action,如此迴圈判斷,一直到nextResponder為nil

關於UIButton的addTarget,更多資訊可檢視對UIButton的addTarget方法探究

回到了上面的程式碼:

當是debug的時候,進入到這個頁面firstResponder等於nil,會走到第4步,而btn顯然沒有實現onTap方法,因此會找它的nextResponder,他的nextResponder是VC的View,也沒有實現onTap方法,但是VC的View的nextResponder是ViewController,實現了onTap方法,因此onTap會被呼叫。
當release的時候,進入到這個頁面firstResponder等於keyWindow,按照上面的步驟,什麼也不會執行。(上面的擴充套件中對UIWindow新增擴充套件,是為了搖一搖,而只在release新增,是因為我們專案中接入了RN,RN在debug中hook了系統的事件,我們專案中做了一些額外的處理,當然不是很重要)
當release的時候,進入到這個頁面並且彈起鍵盤的時候,firstResponder就等於了textField,會走第一步,textField的nextResponder的nextResponder也是VC,因此ViewController的onTap方法也會被呼叫