本文转自:https://www.mikeash.com/pyblog/friday-qa-2009-01-23.html
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:
@"%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
name,
obj,
class_getName([obj class]),
class_getName(obj->isa),
[ClassMethodNames(obj->isa) componentsJoinedByString:@", "]];
printf("%s\n", [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\n",
[control methodForSelector:@selector(setX:)],
[x methodForSelector:@selector(setX:)]);
printf("Using libobjc functions, normal setX: is %p, overridden setX: is %p\n",
method_getImplementation(class_getInstanceMethod(object_getClass(control),
@selector(setX:))),
method_getImplementation(class_getInstanceMethod(object_getClass(x),
@selector(setX:))));
return 0;
}
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
and 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
on Foundation we can get a complete listing of all of these private functions:
0013df80 t __NSSetBoolValueAndNotify
000a0480 t __NSSetCharValueAndNotify
0013e120 t __NSSetDoubleValueAndNotify
0013e1f0 t __NSSetFloatValueAndNotify
000e3550 t __NSSetIntValueAndNotify
0013e390 t __NSSetLongLongValueAndNotify
0013e2c0 t __NSSetLongValueAndNotify
00089df0 t __NSSetObjectValueAndNotify
0013e6f0 t __NSSetPointValueAndNotify
0013e7d0 t __NSSetRangeValueAndNotify
0013e8b0 t __NSSetRectValueAndNotify
0013e550 t __NSSetShortValueAndNotify
0008ab20 t __NSSetSizeValueAndNotify
0013e050 t __NSSetUnsignedCharValueAndNotify
0009fcd0 t __NSSetUnsignedIntValueAndNotify
0013e470 t __NSSetUnsignedLongLongValueAndNotify
0009fc00 t __NSSetUnsignedLongValueAndNotify
0013e620 t __NSSetUnsignedShortValueAndNotify
_NSSetObjectValueAndNotify
) but they need a whole host of functions for the rest. And that host is kind of incomplete: there's no function for
long double
or
_Bool
. There isn't even one for a generic pointer type, such as you'd get if you had a
CFTypeRef
property. 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.)