Swift-漏掉lazy引发的一个神奇的bug

在上线的当前晚上,测试发现有一个按钮点击无响应,测试给出的复现路径是:在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方法也会被调用

猜你喜欢

转载自juejin.im/post/7049305180575105037