最近新版本上线TestFlight,发现下面这个crash突然冒了出来
*** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘An instance 0x of class A was deallocated while key value observers were still registered with it. Current observation info:
( <NSKeyValueObservance 0x: Observer: 0x, Key path: value, Options: <New: NO, Old: NO, Prior: NO> Context: 0x, Property: 0x>
泡上一杯上好的农夫山泉,我们开始了debug之旅。
分析
- 从数据来看,这个crash只在iOS 11以下的设备上出现
crash信息很清楚,
A was deallocated while key value observers were still registered with it
,下面的code可以100%复现这个问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class A: NSObject {
dynamic var value: Int = 0
var observation: NSKeyValueObservation?
override init() {
super.init()
// Crash
self.observation = self.observe(\A.value, options: [.new], changeHandler: { (_, change) in
})
}
}
var a: A? = A()
a = nilcode很简单,我们用Swift 4的新KVO语法去observe A上面的一个属性。通过
a = nil
强制释放,我们得到了相同的crashGoogle之后,发现这是一个已知的iOS 11以下的bug: https://bugs.swift.org/browse/SR-5816
This is happening because NSKeyValueObservation holds a weak reference to an object. That weak reference turns to nil too soon.
修复
找到原因之后,修复变得简单,对于iOS 11以下的版本,在deinit
的时候,我们显示的调用removeObserver
一下
1 | deinit { |
思考
为什么之前的版本没有这个问题?
之前的版本我们的工程师写出了下面的code1
2
3
4
self.observation = self.observe(\A.value, options: [.new], changeHandler: { (_, change) in
self.xxx = xxx
})上面的code是一个经典的循环引用,导致A从来不会被释放,也就从来不会触发这个crash
项目中还有类似的下面的code,为什么不会crash?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class A: NSObject {
dynamic var value: Int = 0
var observation: NSKeyValueObservation?
let b = B()
override init() {
super.init()
self.observation = self.b.observe(\B.value, options: [.new], changeHandler: { (_, change) in
})
}
}
class B: NSObject {
dynamic var value: Int = 0
}
var a: A? = A()
a = nil唯一的区别就是A中现在观察的对象是B上的属性,而不是A的,为什么这样不会导致crash?
对于2中的代码,假如我们把之前的fix加上
1
2
3
4
5deinit {
if let observation = self.observation {
self.b.removeObserver(observation, forKeyPath: "value")
}
}我们发现会造成下面的crash,这又是为什么?
*** Terminating app due to uncaught exception ‘NSRangeException’, reason: ‘Cannot remove an observer <Foundation.NSKeyValueObservation 0x> for the key path “value” from because it is not registered as an observer.’
对于2和3的问题,希望后续有时间可以找到原因,同时也把这个问题抛到了这里:https://stackoverflow.com/questions/54730152/weird-swift-4-closure-based-kvo-bug-on-ios-10-devices