返回

当 Swift 中的 lazy、weak 碰上 NSObject

IOS

前言

Hi Coder,我是 CoderStar!

今天给大家介绍一个我遇到的小坑。过程大概是这样的,一个复用页面通过不同的入口进入,等返回时,有的正常,有的却出现了 Crash,log 信息如下:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFString weakObjectValue]: unrecognized selector sent to instance 0x7fff61b64cf0'

一开始看到这个问题,我是一脸懵的,啥情况?String 怎么会发送了 weakObjectValue 这个消息?通过断点调试发现,崩溃发生在以下代码:

class MyClass {
    weak var delegate: AnyObject?
    
    func doSomething() {
        delegate?.doSomething() // 崩溃点
    }
}

delegate 是一个 weak 修饰的属性,正常情况下,当 delegatenil 时,就不会发送 doSomething 消息了,但是这里却崩溃了,这又是咋回事?

原因分析

经过一番排查,我发现问题出在 lazy 属性上。在我创建 MyClass 实例的时候,使用了 lazy 属性来初始化 delegate

class MyClass {
    lazy var delegate: AnyObject? = {
        return WeakWrapper(object: ... /* 这里省略了具体的初始化逻辑 */)
    }()
    
    func doSomething() {
        delegate?.doSomething() // 崩溃点
    }
}

WeakWrapper 是一个自定义的弱引用包装器,它可以保证在 delegatenil 时,不会发送 doSomething 消息。

然而,问题就出在 lazy 属性上。lazy 属性的初始化是在第一次访问该属性时才进行的。而在我们这个例子中,delegate 属性第一次被访问是在调用 doSomething 方法的时候。但是此时,MyClass 实例已经释放了,导致 delegatenil,从而引发了崩溃。

解决方案

知道了问题的原因,解决方法也就很简单了,只需要将 lazy 属性改成普通属性即可:

class MyClass {
    var delegate: AnyObject? = {
        return WeakWrapper(object: ... /* 这里省略了具体的初始化逻辑 */)
    }()
    
    func doSomething() {
        delegate?.doSomething() // 不再崩溃
    }
}

这样,delegate 属性就会在 MyClass 实例创建的时候就初始化了,就不会出现 delegatenil 的情况了。

总结

需要注意的是,lazy 属性在使用时是有条件限制的,不能用于修饰 weak 属性,否则会引发崩溃。在使用 lazy 属性时,要确保在第一次访问该属性之前,其所属的实例不会被释放。