为啥在另一个线程上发生 dealloc 时尝试创建对 self 的弱引用时会崩溃?

Posted

技术标签:

【中文标题】为啥在另一个线程上发生 dealloc 时尝试创建对 self 的弱引用时会崩溃?【英文标题】:Why am I crashing when trying to create a weak reference to self while dealloc is happening on another thread?为什么在另一个线程上发生 dealloc 时尝试创建对 self 的弱引用时会崩溃? 【发布时间】:2017-11-29 21:14:48 【问题描述】:

背景

我们的应用有一个尝试实现Receptionist Pattern for KVO observation 的类。整个应用程序中的其他类(例如视图控制器)创建这个 Receptionist 类的实例以充当 KVO 观察者。每个 Receptionist 实例保留一份由所有者提供的块的副本,当 KVO 通知到达时,Receptionist 实例将在正确的操作队列上调用该块。

接待员的 dealloc 方法调用 KVO removeObserver 方法。 Owner 将 Receptionist 实例保留为强引用字段,因此当 Owner 被释放时,Receptionist 将在释放过程中将自己作为观察者移除。

崩溃

当一个线程上的 Receptionist 实例收到 KVO 通知,而另一个线程上正在执行同一实例的 dealloc 时,我们看到了来自崩溃领域的报告。接待员对 observeValueForKeyPath:ofObject:change:context: 的实现在这一行崩溃了:

    __weak typeof(self) weakSelf = self;

崩溃报告中的堆栈跟踪显示这是对 objc_initWeak 的调用,它调用了weak_register_no_lock,它又调用了 _objc_fatal。

这个特定的接待员正在观察其键的对象永远不会被释放。所有者也没有被解除分配; Owner 正在用其他实例替换这个 Receptionist 实例。

混乱

我可以理解,创建对已经被释放的对象的弱引用是没有用的,但我希望weakSelf 接收一个 nil 值,而不是导致崩溃。

documentation for objc_initWeak 明确提到如果需要引用的参数已开始解除分配,则将目标设置为 null。这听起来像是期望的行为,但我认为这不是我所看到的。我并不热衷于用对 objc_initWeak 的显式调用来替换该行,因为我怀疑我是否能正确管理释放。

在请求对 self 的弱引用之前,是否真的是接待员的责任注意到它自己的释放正在进行中?我假设在 NSObject 的解除分配开始和调用该对象的 dealloc 方法之间存在一些窗口,因此从 dealloc 方法发出的对象内的信号听起来很不稳定。

感谢您的阅读!

PS:在阅读了 Ken Thomases 提出的问题后进行了大量编辑。

【问题讨论】:

因此,每个 Receptionist 实例都包含对其观察对象的强引用,此引用在初始化期间分配而不会分派到另一个线程,“addObserver:”也在初始化期间调用而不分派到另一个线程,以及“removeObserver:”在释放期间调用? @SergeyAlpeev 是的。我确认接待员对被观察对象的引用是强的,并且在接待员的初始化期间被填充。 addObserver: 也发生在接待员的初始化期间。 removeObserver:在dealloc中调用。一种以前没有注意到的可疑气味:所有属性都使用 self.x = x 而不是 _x = x 初始化(在 addObserver 之前)。似乎仍然不太可能是罪魁祸首。 【参考方案1】:

这与弱引用的创建无关。您引用的这行代码只能在强烈引用 self 的上下文中运行。

想一想:您看到的崩溃可能发生在您的 observeValueForKeyPath:ofObject:change:context: 实现中的那一行,但是,由于在释放和调用该方法之间显然存在竞争,释放也可能在调度期间发生该方法调用(或其他点)。你很容易受到不同的崩溃。因此,对方法的实现没有任何更改可能会解决问题,因为问题可能在您的方法被调用之前就已经显现。

如果您要调用该对象的方法,那么您有责任保持对该对象的强引用。或者,从另一个角度来看,避免在对象指针上调用您不确定是否会在调用期间存在的方法(因为您持有强引用或其他一些 API 保证)。

使用 KVO,您需要在释放最后一个强引用之前移除观察者。

【讨论】:

你关于方法其余部分的观点是有效的,但只要 self 为 nil,它就不会崩溃。我们有接待员的一半原因是观察者的移除作为 ARC 释放的一部分自动处理。对于业主需要警告接待员它即将被取消分配的想法,我真的很难接受。 Apple docs 明确表示“一个典型的模式是......在解除分配期间取消注册(通常在 dealloc 中)”。 什么情况下self可以为nil?你一开始是怎么接触到这种方法的?观察者移除不会自动处理。您引用的相同文档说“观察者在释放时不会自动删除自己。被观察对象继续发送通知,忽略观察者的状态。但是,像任何其他消息一样,更改通知发送到已释放对象,触发内存访问异常。因此,您要确保观察者在从内存中消失之前将自己移除。" 我不知道您认为哪个对象是“所有者”。无论添加什么对象,观察者通常负责 a) 保持对观察者的强引用,并且可能是被观察对象; b) 在释放对它的强引用之前移除观察者。 感谢您的反馈!我已经编辑了这个问题,希望能回答你的问题,但为了便于阅读,我会在这里重复一遍。 Receptionist 是一个在我们的应用程序中使用了几十个地方来简化 KVO 的类。每个所有者为所有者希望观察的每个变量创建一个接待员实例。每个 Receptionist 都作为 Owner 的强引用保存,因此如果 Owner 被解除分配,Receptionist 将被自动解除分配。接待员的 dealloc 方法调用 removeObserver。问题不在于接待员并没有将自己作为观察员。这发生在 dealloc 的末尾。

以上是关于为啥在另一个线程上发生 dealloc 时尝试创建对 self 的弱引用时会崩溃?的主要内容,如果未能解决你的问题,请参考以下文章

为啥套接字被阻止接收,而我睡在另一个线程上?

为啥任务取消发生在调用者线程上?

为啥 Spring 项目的导出的可运行 JAR 不能在另一个系统中工作?

为啥当我尝试在 ios 7.1 sdk、xcode 5.1 中调用 deallocate 或 release 时出现错误?

为啥我的 AsyncTask 在主线程上运行?

为啥不创建单独的线程?