Swift 4 KVO 崩溃:观察对象在观察者仍处于注册状态时已被释放

7
我最近开始开发我的应用程序,目标版本为iOS 11,因为这是默认值。现在我将版本降低到9.3,因为一些原因。
该应用程序是纯Swift 4编写的,使用了新的KVO块。我修复了几个编译时错误,如“safeAreaInsets”等,并且应用程序构建成功。快速完成。不错。
我尝试在iPhone 7 iOS 10.3.1模拟器上运行它,结果崩溃了。我猜以前可能没有使用“UITableViewAutomaticDimension”。
无论如何,我已经解决了大部分布局问题,但现在我卡在了一些难以解决的崩溃问题上。我在导航推送的ViewController中使用了这个新的KVO,当我导航返回时它会崩溃。我的导航推送ViewController在其持有的对象内监听KVO字段。当我弹出导航时,ViewController和对象都被释放,应用程序崩溃,并提示以下错误:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x7fdf2e724250 of class MyProject.MyObject was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x60800003fd80> (
<NSKeyValueObservance 0x610000050020: Observer: 0x61000006f140, Key path: isSelected, Options: <New: NO, Old: NO, Prior: NO> Context: 0x0, Property: 0x6180000595f0>
)'

据我所知,它表示在某人观察变量isSelected时,观察对象MyObject已被释放。这是在MyViewController中观察MyObject的代码。
var observations:[NSKeyValueObservation] = []
func someFunction() {
    observations.removeAll()
    let myObject = MyObject(/*init*/)
    self.myObject = myObject
    observations.append(myObject.observe(\.isSelected, changeHandler: { [weak self] (object, value) in
        //Do stuff
    }))
}

我本以为这种新的KVO-block-style能解决世界和平问题,但显然只适用于iOS 11。

现在,我已经尝试了一些方法,但是我无法阻止它崩溃。每次都会发生,我不知道为什么。

由于崩溃日志告诉我被观察对象在有对象观察它时被释放,但我也知道观察对象在被观察对象之前被释放,所以我尝试在观察者中做如下操作:

//In MyViewController
deinit {
    observations.forEach({$0.invalidate()})
    observations.removeAll()
    print("Observers removed")
}

但这并没有帮助。我也尝试了以下操作:
//In MyObject
deinit{
    print("MyObject deinit")
}

当我执行此操作时,会得到以下输出:

>Observers removed
>MyObject deinit
>WORLD WAR 5 STACK TRACE

我也尝试过

//In MyViewController
deinit{
    self.myObject.removeObserver(self, forKeyPath: "isSelected")
}

但是我得到的输出结果是 无法删除,因为它未注册为观察者。所以我猜测 MyViewController 并不是在使用这个新的 KVO 时被附加的观察者。

为什么这些都没有起作用呢?在 < iOS11 中,我何时需要移除观察者?


(1) 一个常见的问题是将观察者存储在属性中会导致保留循环。你的 deinit 实际上被调用了吗? (2) 你完全正确,运行在 iOS 10 及之前版本上是个问题;在 iOS 11 中,如果观察者或被观察者先消失,就不会有问题。 - matt
无法删除,因为它未注册为观察者。所以我猜MyViewController实际上不是使用这个新的KVO时附加的观察者。正确。观察者就是观察者!你知道的——存储在“observations”中的那个东西。 - matt
@matt 哦,对了!我不知怎么想到观察作为一个对象来跟踪观察者和被观察者,而实际上它并不是观察者本身。是的,两个deinits都被调用了,就像我在问题中展示的那样。看起来根据Charles的回答,我要从我的项目中删除所有Swift-KVO。由于我在一个数组中存储了许多(无序的)观察者,每个观察者都在观察一个单独的无序数组中的对象,所以我认为不需要了。 - Sti
1个回答

12

看起来你遇到了一年前我报告的一个bug,但很不幸,它收到了很少的关注:

https://bugs.swift.org/browse/SR-5752

由于这个bug已经有一段时间没有影响到我了,我曾希望它已经被修复在Swift的覆盖层中,但我刚刚尝试将我的代码从bug报告中复制到一个iOS项目中并在10.3.1模拟器中运行,结果崩溃又出现了。

你可以像这样解决它:

deinit {
    for eachObservation in observations {
        if #available(/*whichever version of iOS fixes this*/) { /* do nothing */ } else {
            self.removeObserver(eachObservation, forKeyPath: #keyPath(/*the key path*/))
        }
        eachObservation.invalidate()
    }

    observations.removeAll()
}

请确保只在受此漏洞影响的iOS版本上执行此操作,否则您会删除一个已经被移除的观察者,然后可能会导致崩溃。这很有趣,不是吗?


天啊,这太恶心了。找出所有受影响的版本需要很长时间。我不想做那个 :( 我能把 self.removeObserver 调用放在一个 try/catch 块中,并在任何 iOS 版本上运行它吗?.. 这样可以节省很多时间.. - Sti
1
@Sti try/catch 无法捕获 Objective-C 异常,这就是此处抛出的异常。此外,据我所知,在 Swift 中没有办法捕获异常,尽管我想你可以轻松地混合使用一个使用 @try@catch 的 Objective-C 文件。如果你这样做,可能会遇到内存泄漏的问题——众所周知,Objective-C 异常在 ARC 下不能完美工作。 - Charles Srstka
那就这样吧。这是我最后一次信任苹果的KVO了。回归ReactiveCocoa或者RXSwift。我还以为Swift 4已经可以用于KVO了,唏嘘。 - Sti
在iOS 12.4和Swift 5中,我不需要在deinit中手动删除观察者。但是当我在iOS 9下运行我的项目时,它崩溃了! - vmeyer
1
相关解决方法,对于在此处寻找答案的人 - 复合关键路径(例如\ .x.y)可能会在取消初始化时导致崩溃,直接观察(x.observe(\ .y))正常工作 - https://drewag.me/posts/2019/09/02/careful-with-fancy-kvo-callback - Manav
如果您查看我链接的错误报告中的示例,您会发现只要您运行在足够旧的 macOS 或 iOS 版本上,即使进行直接观察,它也会崩溃。 - Charles Srstka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接