はじめに
SwiftでのNSObject KVOには、NSKeyValueObservationを使うことがほとんどです。
Using Key-Value Observing in Swift | Apple Developer Documentation
今回、NSKeyValueObservationで発生したクラッシュ調査でいくつか発見があったので、こちらに記しておきます。
ポイント
- NSKeyValueObservation.invalidate()は極力避ける
- NSKeyValueObservationは、nullableで保持し、インスタンス解放で無効化する
なぜ、NSKeyValueObservation.invalidate()は極力避けるのか?
なぜなら、マルチスレッド環境で、NSRangeExceptionを起こすからです。
今回調査していたクラッシュもそれで、以下のようなスタックトレースでクラッシュしていました。
Fatal Exception: NSRangeException
Cannot remove an observer <_NSKeyValueObservation 0x282f97810> for the key path “xxxxx” from <XXXXX 0x28213c9e0> because it is not registered as an observer.
...
0 CoreFoundation 0x1b0a5927c __exceptionPreprocess
1 libobjc.A.dylib 0x1afc339f8 objc_exception_throw
2 CoreFoundation 0x1b09634b0 -[NSCache init]
3 Foundation 0x1b13ff430 -[NSObject(NSKeyValueObserverRegistration) _removeObserver:forProperty:]
4 Foundation 0x1b13ff1ec -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:]
5 Foundation 0x1b14bf48c -[NSOperationQueue removeObserver:forKeyPath:]
6 Foundation 0x1b13fef2c -[NSObject(NSKeyValueObserverRegistration) removeObserver:forKeyPath:context:]
7 libswiftFoundation.dylib 0x1dee06b04 NSKeyValueObservation.invalidate()
8 ***** 0x1059594e4 (Missing)
9 ***** 0x1059585fc (Missing)
10 ***** 0x105956b90 (Missing)
11 ***** 0x10594a4b4 (Missing)
12 libdispatch.dylib 0x1b0498a38 _dispatch_call_block_and_release
13 libdispatch.dylib 0x1b04997d4 _dispatch_client_callout
14 libdispatch.dylib 0x1b047cafc _dispatch_root_queue_drain
15 libdispatch.dylib 0x1b047d248 _dispatch_worker_thread2
16 libsystem_pthread.dylib 0x1b06791b4 _pthread_wqthread
調査の結果、マルチスレッドで、invalidate()が呼ばれると起こることがわかりました。
検証のために、以下のようなサンプルコードで、Playgroundで動作させてみると、invalidate()をマルチスレッドで呼ぶと例外が発生することがわかります。
import Foundation
class MyObjectToObserve: NSObject {
@objc dynamic var myDate = NSDate(timeIntervalSince1970: 0) // 1970
func updateDate() {
myDate = myDate.addingTimeInterval(Double(2 << 30)) // Adds about 68 years.
}
}
class MyObserver: NSObject {
@objc var objectToObserve: MyObjectToObserve
var observation: NSKeyValueObservation?
init(object: MyObjectToObserve) {
objectToObserve = object
super.init()
observation = observe(
\.objectToObserve.myDate,
options: [.old, .new]
) { object, change in
print("myDate changed from: \(change.oldValue!), updated to: \(change.newValue!)")
}
}
}
let observed = MyObjectToObserve()
let observer = MyObserver(object: observed)
observed.updateDate()
/* No problem is to call invalidate() repeatedly by the same thread. */
// observer.observation?.invalidate()
// observer.observation?.invalidate()
let group = DispatchGroup()
(0..<10).forEach { _ in
DispatchQueue.global().async(group: group) {
/* Leads to a crash by NSRangeException */
// observer.observation?.invalidate()
observer.observation = nil
}
}
group.wait()
observed.updateDate()
例外の原因は、こちらです。
Asking to be removed as an observer if not already registered as one results in an NSRangeException. You either call removeObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception.
Key-Value Observing Programming Guide - Removing an Object as an Observer
実際にinvalidate()の中で、removeObserver(_:forKeyPath:context:)が呼ばれています。
一方、observer.observation
へnilをアサインして解放すれば、例外は起こりません。
これは、NSKeyValueObservation.deinitでは、invalidate()が呼ばれますが、マルチスレッドで解放されてもdeinitが一度しか呼ばれないため、例外が発生しないのだと思われます。
そのため、NSKeyValueObservationを扱うときは、何かしらの理由で、let
で宣言しない限りは、invalidate()を呼ばす、単に解放してあげる方法が安全です。
たとえば、あるメソッド内で以下を実装したとき、そのメソッドが必ずシングルスレッドで呼ばれることを将来に渡り保証するのは難しいからですからね。
observation?.invalidate()
observation = nil
余談
NSKeyValueObservationは、Appleの公式サイト ではドキュメントないので、ずっと困っていたのですが、なんと、こちらにありました。
- (old) apple/swift/stdlib/public/Darwin/Foundation/NSObject.swift · GitHub
- swift-corelibs-foundation/Darwin/Foundation-swiftoverlay/NSObject.swift
NSObject.observe(_:keyPath:options:changeHandler:)
やNSKeyValueObservationは、Swiftのstdlibが提供しているNSObject KVO拡張だったのですね。