When is KVO unregistration automatic?

Usually this is not complicated. If we are running on iOS 11+ / macOS 10.13+, KVO unregistration is mostly automatic. If we are running on anything earlier, we must remember to call removeObserver:forKeyPath in our dealloc/deinit.

If we don’t unregister when we are supposed to, we’ll get a crash like this:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x1005bf3a0 of class Foo was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x102804ac0> ( ... )'

While unregistration is mostly automatic on recent OS releases, there are times when it’s not. Let’s dig into how KVO works and figure out when we must unregister.

Just want the summary? Skip to the table at the bottom.

Primer on how KVO works

KVO, or key-value observing, is a magical system that allows us to observe changes to properties of Objective-C objects. I say magical because it completely exploits the Objective-C runtime to make notification happen with minimal work on our part.

There are two states KVO observed objects can be in: manually notifying and autonotifying.

The simple case: manual notifications

With manual notification, the observed object must tell its observers when changes are happening. This is not the default — you have to opt-out to get manual notifications.

See the someString setter below where we call willChangeValue(for:) and didChangeValue(for:) to notify observers:

class Foo: NSObject {
    @objc
    class func automaticallyNotifiesObserversOfSomeString() -> Bool {
        // Opt-out of auto notification for updates to "someString"
        return false
    }

    @objc
    dynamic var someString: String? {
        willSet {
            // Manually tell our observers.
            willChangeValue(for: \.someString)
        }
        didSet {
            // Manually tell our observers.        
            didChangeValue(for: \.someString)
        }
    }
}

var foo = Foo()

var observation = foo.observe(\.someString) { _, _ in
    print("someString changed to: \(foo.someString ?? "nil")")
}

foo.someString = "some value"

When we run this, we see our observer is called when someString is changed:

someString changed to: some value

There are times when we want to control notifications manually (e.g. properties that are derived from other values), but most of the time automatic is what we want. That’s where it gets interesting.

The magic case: automatic notifications

In the automatic case, KVO does a bunch of tricks with the Objective-C runtime to make change notifications fire automatically.

The first time we use KVO to observe an object, the object’s class is changed. Let’s look at what happens with an example.

Suppose we had this tiny class Foo:

class Foo: NSObject {
    @objc
    dynamic var someString: String?
}

And, we printed out the class name of our Foo instance before and after we registered an observer:

var foo = Foo()

print("Class BEFORE observing: \(String(cString: class_getName(object_getClass(foo))))")

var obs = foo.observe(\.someString) { _, _ in
    print("someString changed to: \(foo.someString ?? "nil")")
}

foo.someString = "some value"

print("Class AFTER observing: \(String(cString: class_getName(object_getClass(foo))))")

In the output, we’d find the class changes:

Class BEFORE observing: KVOTestSwift.Foo
someString changed to: some value
Class AFTER observing: NSKVONotifying_KVOTestSwift.Foo

What’s going on here? Well, our foo object is becoming what KVO calls autonotifying.

KVO has created a new class NSKVONotifying_KVOTestSwift.Foo and registered it as a sub-class to our original KVOTestSwift.Foo class. Our foo object’s class is swapped to this new class using object_setClass.

This new NSKVONotifying_ class overrides the setters for any properties that are observed. The new setters wrap our own with calls to willChangeValueFoKey: before and didChangeValueForKey: after so observers get their notifications.

The NSKVONotifying_ class also overrides dealloc / denit with its own implementation in NSKVODeallocate. In cases where unregistration is required, it is NSKVODeallocate that raises the exception if observers have not been removed.

Once an object becomes autonotifying, it will stay that way until the last KVO observation (automatic or manual) is removed. After that, the object’s class will change back to the original implementation.

Here’s a demo where we print our object’s class name before and after we remove the last KVO observer:

var foo = Foo()

var obs: NSKeyValueObservation? = foo.observe(\.someString) { _, _ in
    print("someString changed to: \(foo.someString ?? "nil")")
}

print("Class before removing observer: \(String(cString: class_getName(object_getClass(foo))))")

// Setting to nil will trigger a call to removeObserver:forKeyPath:
obs = nil

print("Class after removing observer: \(String(cString: class_getName(object_getClass(foo))))")

After removal, we see that the class name changes back:

Class before removing observer: NSKVONotifying_KVOTestSwift.Foo
Class after removing observing: KVOTestSwift.Foo

That’s enough of a primer; let’s get back on topic — when is removal necessary.

When is unregistration automatic?

Back in Foundation’s release notes for macOS 10.13 and iOS 11, Apple had this to say about automatic unregistration:

Relaxed Key-Value Observing Unregistration Requirements

Prior to 10.13, KVO would throw an exception if any observers were still registered after an autonotifying object’s -dealloc finished running. Additionally, if all observers were removed, but some were removed from another thread during dealloc, the exception would incorrectly still be thrown. This requirement has been relaxed in 10.13, subject to two conditions:

  • The object must be using KVO autonotifying, rather than manually calling -will and -didChangeValueForKey: (i.e. it should not return NO from +automaticallyNotifiesObserversForKey:)
  • The object must not override the (private) accessors for internal KVO state

If all of these are true, any remaining observers after -dealloc returns will be cleaned up by KVO; this is also somewhat more efficient than repeatedly calling -removeObserver methods.

Here’s that same information cooked down to a table with some important caveats noted:

On macOS 10.13 / iOS 11 or later:

KVO object state1 Overrides observationInfo2  
Autonotifying No Unregistration Automatic
Autonotifying Yes :boom: Unregistration Required
Non-autonotifying3 No Unregistration Automatic
Non-autonotifying3 Yes Unregistration Automatic


On macOS 10.12 / iOS 10 or earlier:

KVO object state1 Overrides observationInfo2  
Autonotifying No :boom: Unregistration Required
Autonotifying Yes :boom: Unregistration Required
Non-autonotifying3 No Unregistration Automatic
Non-autonotifying3 Yes Unregistration Automatic
  1. An object becomes autonotifying as soon as the first observation is added for an automatically notifying key path. It stays autonotifying until all observations (for manual or automatically notifying key paths) are removed.

  2. Does the class override the observationInfo property used to store internal KVO state?

  3. An object may become unexpectedly autonotifying depending on how others use the object.

An object may unexpectedly become autonotifying

This is the case that can bite us and the original motivation for this entire deep dive.

Maybe we wrote a class that opted out of automatic notifications for all of its properties. We used KVO with this object but never bothered to unregister and things were working fine.

Later, someone else subclasses our class and adds autonotifying key paths.

Now we could be in trouble. If we’re running on macOS 10.12 / iOS 10 or earlier, or if the class overrides observationInfo, the unregistration requirements have now changed.

If someone adds an observer for an automatically notifying key path — :boom: — now all KVO observations must be removed before dealloc. Even those for the manually notifying key paths that we never had to worry about before.

Here’s the whole scenario in code.

Our setup: a class which only has a manually notifying key path, and a sub-class of that which adds an autonotifying key path:

/// FooManual exposes `manualNotifyingString` via KVO with manual notifications.
class FooManual: NSObject {
    var _observationInfo: UnsafeMutableRawPointer?

    /// Overriding `observationInfo` makes unregistration required for 
    /// autonotifying key paths even on macOS 10.13+ / iOS 11+.
    override var observationInfo: UnsafeMutableRawPointer? {
        get {
            return _observationInfo
        }
        set {
            _observationInfo = newValue
        }
    }

    @objc
    class func automaticallyNotifiesObserversOfManualNotifyingString() -> Bool {
        return false
    }

    @objc
    dynamic var manualNotifyingString: String?
}

/// FooAuto adds the autonotifying key path of `autoNotifyingString` 
class FooAuto: FooManual {
    @objc
    dynamic var autoNotifyingString: String?
}

First, let’s show that we can get away with not unregistering if we only observe the manually notifying key path:

var obsForManual: NSKeyValueObservation?
var foo: FooAuto?

foo = FooAuto()

obsForManual = foo!.observe(\.manualNotifyingString) { _, _ in
    // manualNotifyingString changed.
}

// Deallocate `foo`; won't crash even though `obsForManual`
// has not been removed / deallocated.  No autonotifying
// key paths were observed.
foo = nil

Now, let’s observe the manual and automatic key path forcing the object to become autonotifying. Once that happens, auto unregistration stops working even for the manually notifying key path:

var obsForManual: NSKeyValueObservation?
var obsForAuto: NSKeyValueObservation?
var foo: FooAuto?

foo = FooAuto()

obsForManual = foo!.observe(\.manualNotifyingString) { _, _ in
    // manualNotifyingString changed.
}

obsForAuto = foo!.observe(\.autoNotifyingString) { _, _ in
    // autoNotifyingString changed.
}

// `foo` is autonotifying at this point!

obsForAuto = nil

// `foo` is STILL autonotifying even after `obsForAuto` is 
// deallocated (which calls `removeObserver:forKeyPath:`).

// Deallocate `foo` without removing the observer for
// `manualNotifyingString` ...

foo = nil // !!! EXCEPTION !!!

In the exception, KVO is going to complain that we never unregistered for that manually notifying key path:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x10295a3c0 of class KVOTestSwift.FooAuto was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x102958170> (
<NSKeyValueObservance 0x10295b730: Observer: 0x10295b6c0, Key path: manualNotifyingString, Options: <New: NO, Old: NO, Prior: NO> Context: 0x0, Property: 0x102956bf0>
)'

Wrapping up

Relying on auto unregistration can bite us under the wrong circumstances.

If we’re working with a third-party class and that class happens to override observationInfo, auto unregistration won’t happen even on macOS 10.13 / iOS 11.

If we’re targeting macOS 10.12 / iOS 10 or earlier, we might get away with not unregistering if the key paths observed happened to be manually notifying. But, that’s a private implementation detail that could change. Or, the object could become autonotifying later if we or someone else observe an auto notifying key path.

If we’re only dealing with first-party code, on macOS 10.13+ / iOS 11+, with no funky observationInfo overrides, it’s going to be smooth sailing.

Discussion and feedback