在上线的当前晚上,测试发现有一个按钮点击无响应,测试给出的复现路径是:在release包里,进入到对应界面,键盘弹出按钮可正常点击,键盘收起按钮无法点击,debug包也可以正常点击。如果你对这个bug感兴趣,点击下载最简化的demo.
其中可复现的核心代码如下
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?
当然,答案一定是肯定会出现上面的问题。当然你可能会有以下的疑问?
- 为什么debug的时候onTap方法会被调用?
- 为什么键盘弹起的时候onTap方法会被调用?
在上面的代码中,btn本意是要写成懒加载的,结果由于复制失误,导致复制的时候,少复制了一个lazy,导致btn变成了非懒加载的存储变量,后面变成了一个立即执行的闭包。在闭包执行的时候ViewController还未进行初始化完成,闭包中的self其实是一个Function, 由于btn的target会尝试转为NSObject,这里一定转化不成功,我们这里简单认定为btn的target等于nil。
也就是说上面的代码可以简单的等同于btn.addTarget(nil, action: #selector(onTap), for: .touchUpInside)
.
而当对一个按钮添加事件的时候,如果target为nil,系统会进行一下的操作
- 首先获取APP当前的firstResponder,如果firstResponder为nil,则进行第4步,否则进行下一步
- 判断firstResponder是否实现此target对应的action,如果实现了,则进行调用。
- 否则则判断firstResponder的nextResponder是否实现,如此循环判断,一直到nextResponder为nil
- 判断btn是否实现了对应的action,如果实现,则进行调用。
- 否则则判断btn的nextResponder是否实现了对应的action,如此循环判断,一直到nextResponder为nil
关于UIButton的addTarget,更多信息可查看对UIButton的addTarget方法探究
回到了上面的代码: