在上线的当时晚上,测验发现有一个按钮点击无呼应,测验给出的复现途径是:在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?

当然,答案一定是肯定会出现上面的问题。当然你可能会有以下的疑问?

  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办法也会被调用