Swift 4中KVO的bug

最近新版本上线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之旅。

分析

  1. 从数据来看,这个crash只在iOS 11以下的设备上出现
  2. 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
    17
    class A: NSObject {
    @objc 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 = nil

    code很简单,我们用Swift 4的新KVO语法去observe A上面的一个属性。通过a = nil强制释放,我们得到了相同的crash

  3. Google之后,发现这是一个已知的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
2
3
4
5
6
7
8
9
deinit {
if #available(iOS 11.0, *) {

} else {
if let observation = self.observation {
self.removeObserver(observation, forKeyPath: "value")
}
}
}

思考

  1. 为什么之前的版本没有这个问题?
    之前的版本我们的工程师写出了下面的code

    1
    2
    3
    4

    self.observation = self.observe(\A.value, options: [.new], changeHandler: { (_, change) in
    self.xxx = xxx
    })

    上面的code是一个经典的循环引用,导致A从来不会被释放,也就从来不会触发这个crash

  2. 项目中还有类似的下面的code,为什么不会crash?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class A: NSObject {
    @objc 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 {
    @objc dynamic var value: Int = 0
    }

    var a: A? = A()
    a = nil

    唯一的区别就是A中现在观察的对象是B上的属性,而不是A的,为什么这样不会导致crash?

  3. 对于2中的代码,假如我们把之前的fix加上

    1
    2
    3
    4
    5
    deinit {
    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

Thank you for making the world a better place!