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.