Swift弱属性线程安全问题:EXC_BAD_ACCESS崩溃原因排查
问题背景
以下是实现的Foo类代码:
enum FooState { case open case closed } protocol FooDelegate: AnyObject { func foo(_ foo: Foo, didChange state: FooState) } class Foo: NSObject { private let queue = DispatchQueue(label: "com.test.fooQueue") private(set) var state: FooState? { didSet { guard let state = state else { return } delegate?.foo(self, didChange: state) } } weak var delegate: FooDelegate? func close() { // Do some extra logic here... didChangeFooState(.closed) } private func didChangeFooState(_ state: FooState) { queue.async { [weak self] in self?.state = state } } }
在App主队列中调用:
foo.close() foo = nil
大多数情况下代码正常运行,但偶尔会在delegate?.foo(self, didChange: state)行触发EXC_BAD_ACCESS KERN_INVALID_ADDRESS崩溃,疑问如下:
- 崩溃的原因是什么?我的猜测是
state在后台队列赋值,而foo在主队列被销毁引发竞态条件,是否正确? - 引用计数是原子性的,这会对该问题产生影响吗?
- 改用
guard let delegate而非可选链式调用,能否解决问题?
问题分析与解答
崩溃原因
你的猜测完全正确,这是竞态条件导致的崩溃:
- 主队列调用
foo.close()后立刻执行foo = nil,此时Foo实例的引用计数被减至0,主队列随即开始销毁该实例。 - 与此同时,
com.test.fooQueue队列中的异步任务可能还在等待执行,或者刚好在实例销毁的过程中执行self?.state = state。此时Foo实例的内存已经开始被回收,触发didSet后访问self或调用delegate方法时,就会访问到已释放的内存,引发EXC_BAD_ACCESS崩溃。
关于引用计数原子性的疑问
引用计数的原子性确实能保证引用计数的增减操作是线程安全的,但它无法解决对象访问的竞态问题:
- 原子性只是确保多个线程对引用计数的修改不会出现混乱,但当引用计数降至0后,对象的销毁过程是在触发销毁的队列(这里是主队列)上执行的,这个过程和其他队列对对象的访问操作之间没有同步机制,仍然可能出现一个线程在销毁对象,另一个线程同时访问对象的情况。
改用guard let delegate能否解决问题?
不能。崩溃的核心原因是Foo实例本身的内存被提前释放,而非delegate的访问问题。即使你提前用guard let delegate = delegate拿到强引用,调用delegate.foo(self, didChange: state)时,self仍然可能是已被销毁的野指针,崩溃依然会发生。
内容的提问来源于stack exchange,提问作者Anton
相关产品推荐
相关产品推荐

