KVO

In my last post, I explained why you should prefer delegates over observers. The former encourages a tree-like data flow, while the latter leads to a cat's cradle.

If you're sure you need an observer, Cocoa lets you choose between Notification Center and KVO. Which should you use? That's easy. Notification Center. Avoid KVO. Thanks, and see you next month!

Learning Cocoa, a lot of great engineers told me, "don't use KVO," and left it at that. Maybe they didn't want to get into a lecture about architecture, and for years, implementation issues were enough  dealbreakers:

  • The single method signature is error-prone
  • The context parameter is error-prone
  • String typing is error-prone
  • Performance is unpredictable

On the performance front, things are getting better, with iOS 9 fixing many issues. On key-path safety, you can add simple compile time checks.

But even with a full rewrite, KVO's underlying pattern makes things are harder to maintain and debug. Ad-hoc property-observation is just a bad idea.

Other platforms have gone down this road. Consider JavaScript's failed Object.observe proposal, which its authors withdrew:

Complex apps ended up "creating tens of thousands of O.o calls," he told InfoQ. "This was tricky to debug, and had strange performance characteristics. On Chrome, setup time was long but runtime was fast. On polyfilled browsers, the opposite was true."

KVO sounded like a great idea, so why the dead end?

Hidden Side Effects

Consider this innocuous code:

func updateFromForm(form) {
  self.avatar = form.image
  self.firstName = form.firstName
  self.lastName = form.lastName
}

You wouldn't know there are hidden side effects from updating that avatar property. Maybe it updates a tableview cell, and maybe that incurs an expensive draw operation. In KVO heavy apps, you spend an inordinate ammount of time wondering, "What is doing this?"

Now consider:

func updateFromForm(form) {
  self.avatar = form.image
  self.firstName = form.firstName
  self.lastName = form.lastName
  NotificationCenter.default.
    post(name:.UserDidUpdate, object:self)
}

Notifications may be a glorified goto statement, but at least you can follow the jump. KVO is like the joke programming construct COMEFROM.1

Notifications explicitly yield control. While we don't know the consequences, if we're curious, we can search for instances of that notification. That little hint is the difference between 'annoying to follow' and 'spooky action from a distance.'2

The Wire Tap

Matt Drance said, "The observer pattern typically involves a defined contract. KVO’s worst smell for me is that it’s more of a wiretap."

KVO makes it very easy to poke holes in encapsulation. We see this all the time with sneaky iOS developers observing views in the iOS keyboard. You shouldn't dig through Apple's private view hierarchies, and you certainly shouldn't rely on their internal behavior.

Regardless of whether the object belongs to Apple or yourself, KVO fools you into thinking you have an API when you don't. In some future version, code churn changes internal behavior, and the assumptions you built your hacky API on fly out the window.

KVO reminds me of Rails' delight with Monkey Patching, Ruby's version of swizzling. "We don't need formal APIs," proclaimed the 10x developers, "we can build our plugins on top of monkey patches!" Then Rails would release an update, and plugins relying on internal behavior broke.

Say you're writing an email client, and there's a MailSender class outside your control:

class MailSender:NSObject {
    var outbound = Queue<Message>()
    var sending:Bool = false
    dynamic var isIdle:Bool {
        return outbound.count == 0 && sending == false
    }

    func handleNextMessage() {
        guard let message = outbound.nextItem() else {
            return
        }
        sending = true
        message.send()
        sending = false
        outbound.dequeue()
    }
}

You're writing a mail-throttler. You measure how long it takes to send an email, and if things are moving fast, enqueue more email. You do it by measuring the time between handleNextMessage, and when isIdle becomes true.

"A perfect situation for KVO," you think. Unfortunately, your throttler can't observe isIdle directly, because it isn't key-value compliant.

"Wait, we can observe the outbound property? Close enough!"

It works fine, until the owner of MailSender refactors.

func handleNextMessage() {
  guard let message = outbound.dequeue() else {
    return
  }
  sending = true
  message.send()
  sending = false
}

Now when our observer is called, isIdle is incorrect--- the message has been popped off the queue, but it's still in-flight. Your throttler thinks messages are sent instantaneously. Bugs ensue.

Next, the author of MailSender notices the momentary inconsistency, but only when multithreading. How did do most people fix threading? Slap a lock on it!

class MailSender:NSObject {
    var queueLock = NSLock()
    var outbound = Queue<Message>()
    dynamic var isIdle:Bool {
        queueLock.lock()
        let result = outbound.count == 0 && sending == false
        queueLock.unlock()
        return result
    }
    func handleNextMessage() {
        queueLock.lock()
        defer { queueLock.unlock() }
        guard let message = outbound.dequeue() else {
            return
        }
        message.send()
    }
}

Now your observer deadlocks. If you can find a way around that, it gets better.

Why did the author make the class threadsafe? Because they're moving the operation off the main thread! Since KVO notifications fire on the thread that calls the setter, suddenly you're touching UI from the background.

The real solution is to make isIdle KVO compliant, and document it as such.

func handleNextMessage() {
    self.willChangeValueForKey("isIdle")
    defer { self.didChangeValueForKey("isIdle") }
    queueLock.lock()
    defer { queueLock.unlock() }
    guard let message = outbound.dequeue() else {
        return
    }
    message.send()
}

The problem is that people don't write safe, reentrant code by default. Compare the complexity of the final, KVO compliant version with the simplicity of the first version.

In contrast, it's usually safe to fire a notification at the end of method, after state settles:

func handleNextMessage() {
    defer { self.fireNotification() }
    queueLock.lock()
    defer { queueLock.unlock() }
    guard let message = outbound.dequeue() else {
        return
    }
    message.send()
}

func fireNotification(){
  NotificationCenter.default.
    post(name:.MailSenderDidUpdate, object:self)
}

When we define MailSenderDidUpdateNotification, we make a contract about its behavior. If it changes, we're on the hook. We can document, test, and deprecate it as need be.

If the object you're observing does not document itself as KVO compatible, don't assume it is. Unfortunately, that brings us to KVO's worst flaw.

The Accidental Programming Interface

UIKit's classes do not support KVO. According to the documentation (emphasis mine):

Note: Although the classes of the UIKit framework generally do not support KVO, you can still implement it in the custom objects of your application, including custom views.

Or from Apple Staff on the developer forums:

In general UIKit objects do not support KVO. Unless a class is documented as supporting KVO, you should assume it does not (even if it appears to work – there are likely edge cases that you haven't noticed).

I bet most iOS developers don't know that. Some do, but think it's "safe enough." Meanwhile, if a poor UIKit engineer makes a change in this unsupported behavior, you'd see a mob outside Apple HQ with torches and pitchforks.

KVO's biggest mistake was baking support directly into NSObject. Most of the time it just works. But if nobody is actually checking, compatibility is an accident.

Swift uses a better default. You have to mark properties with dynamic for the machinery to work, making KVO opt-in. Maybe it would be clearer if they were marked observable, but given dynamic dispatch is mostly used for KVO and swizzling, when you mark it dynamic you have a good idea of what you're getting into.

Conclusions

Sometimes, you have no choice but to use KVO, like if you're using AVFoundation, NSOperation, and NSProgress. Proceed with caution.

If you're designing an API yourself, and you're sure you should use the observer pattern, use notification center.

Thanks to everyone who replied to my tweet asking for input.

Subscribe to Sandofsky

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe