Friday Q&A 2009-01-23

Welcome to the first Friday Q&A; of the new Presidential administration. Unlike Mr. Obama, I'm afraid of change and so this week's edition will be just like all the other ones. This week I'll be taking Jonathan Mitchell's suggestion to talk about how Key-Value Observing (KVO) is actually implemented at the runtime level.

What Is It?

Most readers probably know this already, but just for a quick recap: KVO is the technology that underlies Cocoa Bindings, and it provides a way for objects to get notified when the properties of other objects are changed. One object observes a key of another object. When the observed object changes the value of that key, the observer gets notified. Pretty straightforward, right? The tricky part is that KVO operates with no code needed in the object being observed... usually.

Overview

So how does that work, not needing any code in the observed object? Well it all happens through the power of the Objective-C runtime. When you observe an object of a particular class for the first time, the KVO infrastructure creates a brand new class at runtime that subclasses your class. In that new class, it overrides the set methods for any observed keys. It then switches out the isa pointer of your object (the pointer that tells the Objective-C runtime what kind of object a particular blob of memory actually is) so that your object magically becomes an instance of this new class.

The overridden methods are how it does the real work of notifying observers. The logic goes that changes to a key have to go through that key's set method. It overrides that set method so that it can intercept it and post notifications to observers whenever it gets called. (Of course it's possible to make a modification without going through the set method if you modify the instance variable directly. KVO requires that compliant classes must either not do this, or must wrap direct ivar access in manual notification calls.)

It gets trickier though: Apple really doesn't want this machinery to be exposed. In addition to setters, the dynamic subclass also overrides the -class method to lie to you and return the original class! If you don't look too closely, the KVO-mutated objects look just like their non-observed counterparts.

Digging Deeper

Enough talk, let's actually see how all of this works. I wrote a program that illustrates the principles behind KVO. Because of the dynamic KVO subclass tries to hide its own existence, I mainly use Objective-C runtime calls to get the information we're looking for.

Here's the program:

// gcc -o kvoexplorer -framework Foundation kvoexplorer.m #import <Foundation/Foundation.h> #import <objc/runtime.h> @interface TestClass : NSObject { int x ; int y ; int z ; } @property int x ; @property int y ; @property int z ; @end @implementation TestClass @synthesize x , y , z ; @end static NSArray * ClassMethodNames ( Class c ) { NSMutableArray * array = [ NSMutableArray array ]; unsigned int methodCount = 0 ; Method * methodList = class_copyMethodList ( c , & methodCount ); unsigned int i ; for ( i = 0 ; i < methodCount ; i ++ ) [ array addObject : NSStringFromSelector ( method_getName ( methodList [ i ]))]; free ( methodList ); return array ; } static void PrintDescription ( NSString * name , id obj ) { NSString * str = [ NSString stringWithFormat : @"%@: %@

\t NSObject class %s

\t libobjc class %s

\t implements methods <%@>" , name , obj , class_getName ([ obj class ]), class_getName ( obj -> isa ), [ ClassMethodNames ( obj -> isa ) componentsJoinedByString : @", " ]]; printf ( "%s

" , [ str UTF8String ]); } int main ( int argc , char ** argv ) { [ NSAutoreleasePool new ]; TestClass * x = [[ TestClass alloc ] init ]; TestClass * y = [[ TestClass alloc ] init ]; TestClass * xy = [[ TestClass alloc ] init ]; TestClass * control = [[ TestClass alloc ] init ]; [ x addObserver : x forKeyPath : @"x" options : 0 context : NULL ]; [ xy addObserver : xy forKeyPath : @"x" options : 0 context : NULL ]; [ y addObserver : y forKeyPath : @"y" options : 0 context : NULL ]; [ xy addObserver : xy forKeyPath : @"y" options : 0 context : NULL ]; PrintDescription ( @"control" , control ); PrintDescription ( @"x" , x ); PrintDescription ( @"y" , y ); PrintDescription ( @"xy" , xy ); printf ( "Using NSObject methods, normal setX: is %p, overridden setX: is %p

" , [ control methodForSelector : @selector ( setX :)], [ x methodForSelector : @selector ( setX :)]); printf ( "Using libobjc functions, normal setX: is %p, overridden setX: is %p

" , method_getImplementation ( class_getInstanceMethod ( object_getClass ( control ), @selector ( setX :))), method_getImplementation ( class_getInstanceMethod ( object_getClass ( x ), @selector ( setX :)))); return 0 ; }

Let's walk through it, top to bottom.

First we define a class called TestClass which has three properties. (KVO works on non- @property keys too but this is the simplest way to define pairs of setters and getters.)

Next we define a pair of utility functions. ClassMethodNames uses Objective-C runtime functions to go through a class and get a list of all the methods it implements. Note that it only gets methods implemented directly in that class, not in superclasses. PrintDescription prints a full description of the object passed to it, showing the object's class as obtained through the -class method as well as through an Objective-C runtime function, and the methods implemented on that class.

Then we start experimenting using those facilities. We create four instances of TestClass, each of which will be observed in a different way. The x instance will have an observer on its x key, similar for y , and xy will get both. The z key is left unobserved for comparison purposes. And lastly the control instance serves as a control on the experiment and will not be observed at all.

Next we print out the description of all four objects.

After that we dig a little deeper into the overridden setter and print out the address of the implementation of the -setX: method on the control object and an observed object to compare. And we do this twice, because using -methodForSelector: fails to show the override. KVO's attempt to hide the dynamic subclass even hides the overridden method with this technique! But of course using Objective-C runtime functions instead provides the proper result.

Running the Code

So that's what it does, now let's look at a sample run:

control : < TestClass : 0x104b20 > NSObject class TestClass libobjc class TestClass implements methods < setX :, x , setY :, y , setZ :, z > x : < TestClass : 0x103280 > NSObject class TestClass libobjc class NSKVONotifying_TestClass implements methods < setY :, setX :, class , dealloc , _isKVOA > y : < TestClass : 0x104b00 > NSObject class TestClass libobjc class NSKVONotifying_TestClass implements methods < setY :, setX :, class , dealloc , _isKVOA > xy : < TestClass : 0x104b10 > NSObject class TestClass libobjc class NSKVONotifying_TestClass implements methods < setY :, setX :, class , dealloc , _isKVOA > Using NSObject methods , normal setX : is 0x195e , overridden setX : is 0x195e Using libobjc functions , normal setX : is 0x195e , overridden setX : is 0x96a1a550

TestClass

First it prints our control object. As expected, its class isand it implements the six methods we synthesized from the class's properties.

Next it prints the three observed objects. Note that while -class is still showing TestClass , using object_getClass shows the true face of this object: it's an instance of NSKVONotifying_TestClass . There's your dynamic subclass!

Notice how it implements the two observed setters. This is interesting because you'll note that it's smart enough not to override -setZ: even though that's also a setter, because nobody has observed it. Presumably if we were to add an observer to z as well, then NSKVONotifying_TestClass would suddenly sprout a -setZ: override. But also note that it's the same class for all three instances, meaning they all have overrides for both setters, even though two of them only have one observed property. This costs some efficiency due to passing through the observed setter even for a non-observed property, but Apple apparently thought it was better not to have a proliferation of dynamic subclasses if each object had a different set of keys being observed, and I think that was the correct choice.

And you'll also notice three other methods. There's the overridden -class method as mentioned before, the one that tries to hide the existence of this dynamic subclass. There's a -dealloc method to handle cleanup. And there's a mysterious -_isKVOA method which looks to be a private method that Apple code can use to determine if an object is being subject to this dynamic subclassing.

Next we print out the implementation for -setX: . Using -methodForSelector: returns the same value for both. Since there is no override for this method in the dynamic subclass, this must mean that -methodForSelector: uses -class as part of its internal workings and is getting the wrong answer due to that.

So of course we bypass that altogether and use the Objective-C runtime to print the implementations instead, and here we can see the difference. The original agrees with -methodForSelector: (as of course it should), but the second is completely different.

Being good explorers, we're running in the debugger and so can see exactly what this second function actually is:

( gdb ) print ( IMP ) 0x96a1a550 $ 1 = ( IMP ) 0x96a1a550 < _NSSetIntValueAndNotify >

nm -a

0013 df80 t __NSSetBoolValueAndNotify 000 a0480 t __NSSetCharValueAndNotify 0013e120 t __NSSetDoubleValueAndNotify 0013e1 f0 t __NSSetFloatValueAndNotify 000e3550 t __NSSetIntValueAndNotify 0013e390 t __NSSetLongLongValueAndNotify 0013e2 c0 t __NSSetLongValueAndNotify 000 89 df0 t __NSSetObjectValueAndNotify 0013e6 f0 t __NSSetPointValueAndNotify 0013e7 d0 t __NSSetRangeValueAndNotify 0013e8 b0 t __NSSetRectValueAndNotify 0013e550 t __NSSetShortValueAndNotify 000 8 ab20 t __NSSetSizeValueAndNotify 0013e050 t __NSSetUnsignedCharValueAndNotify 0009f cd0 t __NSSetUnsignedIntValueAndNotify 0013e470 t __NSSetUnsignedLongLongValueAndNotify 0009f c00 t __NSSetUnsignedLongValueAndNotify 0013e620 t __NSSetUnsignedShortValueAndNotify

_NSSetObjectValueAndNotify

long double

_Bool

CFTypeRef

It's some sort of private function that implements the observer notification. By usingon Foundation we can get a complete listing of all of these private functions:There are some interesting things to be found in this list. First, you'll notice that Apple has to implement a separate function for every primitive type that they want to support. They only need one for Objective-C objects () but they need a whole host of functions for the rest. And that host is kind of incomplete: there's no function foror. There isn't even one for a generic pointer type, such as you'd get if you had aproperty. And while there are several functions for various common Cocoa structs, there obviously aren't any for the huge universe of other structs out there. This means that any properties of these types will simply be ineligible for automatic KVO notification, so beware!

KVO is a powerful technology, sometimes a little too powerful, especially when automatic notification is involved. Now you know exactly how it all works on the inside and this knowledge may help you decide how to use it or to debug it when it goes wrong.

If you plan to use KVO in your own application you may want to check out my article on Key-Value Observing Done Right.

Wrapping Up

That's it for this week. Will Mike face down the terrifying code monster? Will his IDE finish compiling in time? Tune in next week for another exciting installment! In the meantime, post your thoughts below.

And as a reminder, Friday Q&A; is run by your generous donations. No, not money, just ideas! If you have a topic you would like to see discussed here, post it in the comments or e-mail it directly. (Your name will be used unless you ask me not to.)

Did you enjoy this article? I'm selling whole books full of them! Volumes II and III are now out! They're available as ePub, PDF, print, and on iBooks and Kindle. Click here for more information

Comments:

Add your thoughts, post a comment:

Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.

JavaScript is required to submit comments due to anti-spam measures. Please enable JavaScript and reload the page.