翻译自:https://www.mikeash.com/pyblog/friday-qa-2009-01-23.html (一篇很有年代感的Q&A)
转载请标注。
KVO是啥?
大多数读者可能已经有了解过,那就快速复习一下:KVO是一种基于Cocoa框架的技术,使用它能让一个对象的某些属性发生改变时通知到另外一个对象。对象1监听了对象2的一个key,当对象2的key对应的值发生改变时,对象1就知道了这件事。是不是很简单?KVO最猥琐的操作是通常情况下对象2并不需要增加任何代码。
大概说说
咋做到被监听对象可以不需要任何代码就能实现这效果呢?使用OC的运行时机制就可以啦。当你第一次监听一个特殊类型的对象的时候,KVO内部会通过runtime创建一个这个类的子类。在新建的这个类中重写了你监听的key对应属性的set方法。然后会断开被监听对象结构体的isa指针(这个指针的用作是告诉runtime这个类在内存当中具体存在形式),因此被监听对象的类型变成了新创建的子类的类型。
关于重写的set方法实现监听的逻辑是如果改变key对应的值时一定会走这个key对应的set方法。重写后无论何时都可以在方法里拦截并且发送通知给监听对象。(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.这句没懂?)
苹果其实很不想让大家知道有这样的机制,就想出了一个猥琐的办法。就像重写set方法一样,这个动态生成的子类也会重写 - class 方法,然后返回原来的类来“误导”你。如果你没有深究,就会觉得被监听的对象好像什么事都没做一样。
深挖
让我没看看到底是如何实现的。我写了一段代码来解释一下KVO的内部实现。因为KVO生成的动态子类隐藏了它自己,我就用runtime机制去获取它们的真实信息。
// gcc -o kvoexplorer -framework Foundation kvoexplorer.m#import#import@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;
}
一步步来,从上到下。
第一步我们定义了一个叫TestClass的类,类中定义了三个属性。(KVO对不是属性的key也是有作用的,例子这么写是为了方便定义它们的set和get方法。)
第二步我们定义了两个全局方法。ClassMethodNames方法通过runtime可以拿到一个类的方法实现列表。注意这里只会拿到这个类的方法实现,不包括它的子类。PrintDescription方法会打印这个对象的所有描述信息,包括- class以及这个对象的类的方法实现。
然后我们开始试试。先创建四个TestClass实例,每个实例都会被相应添加监听。实例对象x的x属性会被监听,相应的实例y、xy都会。为了方便比较,所有实例的属性z都没被监听。最后一个实例control啥事没有。
接下来打印一下这四个对象的description。
然后我们针对重写的set方法,比较打印出的control对象和被监听对象的-setX:方法的实现地址。我们要做两遍,因为使用-methodForSelector:体现不出有没有被重写。KVO试图隐藏动态子类甚至想隐藏重写的方法。但是通过runtime还是可以得到真实结果。
跑一跑
一下是代码运行结果:
第一个打印的是对象control。和预想的一样,这个对象是TestClass类型并且义工有六个方法实现。
接下来打印的是三个被监听的对象。注意- class都是显示的是TestClass,使用object_getClass方法的话会发现,其实都是NSKVONotifying_TestClass对象的实例。就是那个动态子类!
着重看一下它是如何实现两个被监听属性的set方法。你会发现它竟然很机智地不去重写同样是set方法的- setZ:方法,当然原因是没有任何对象监听了属性z。如此推测,如果同样对z添加监听,那么NSKVONotifying_TestClass类肯定会重写- setZ:方法。但再看另外三个同样类型并且都被监听的对象,set方法全都被重写,及时它们分别只有一个属性被添加了监听。不管有没有被监听都会被重写set方法的做法会造成一些效率问题,但是苹果好像觉得这样比起每个动态子类都可能存在不一样的set方法来得好,当然,我也这么觉得。
然后你会注意到其他三个方法。- class方法也被重写了,之前有说到的七种一个原因是像隐藏这个动态子类的存在。- deallc方法的重写是为了清除set方法中的通知。这里还有一个完全不认识的-_isKVOA方法,看上去像是一个苹果代码可以决定这个对象是否需要生成动态子类的私有方法。
接下来我们打印一下- setX:的具体实现。使用-methodForSelector:来调用返回的是两个同样的值。 因为在动态子类中没有重写这个-methodForSelector:方法,那就意味着这种方式并不会得到真正的结果。
那我们就绕开这些,使用runtime来打印方法实现,这样就能发现具体的区别在哪。第一个结果和-methodForSelector:返回的一致,但是第二个就完全不一样了。
再深入一点,我们用调试器run一run:
(gdb)print(IMP)0x96a1a550
$1=(IMP)0x96a1a550<_NSSetIntValueAndNotify>
在实现的监听通知中有一些私有的函数,使用nm -a能获取到包含所有私有函数的列表信息。
0013df80t__NSSetBoolValueAndNotify
000a0480t__NSSetCharValueAndNotify
0013e120t__NSSetDoubleValueAndNotify
0013e1f0t__NSSetFloatValueAndNotify
000e3550t__NSSetIntValueAndNotify
0013e390t__NSSetLongLongValueAndNotify
0013e2c0t__NSSetLongValueAndNotify
00089df0t__NSSetObjectValueAndNotify
0013e6f0t__NSSetPointValueAndNotify
0013e7d0t__NSSetRangeValueAndNotify
0013e8b0t__NSSetRectValueAndNotify
0013e550t__NSSetShortValueAndNotify
0008ab20t__NSSetSizeValueAndNotify
0013e050t__NSSetUnsignedCharValueAndNotify
0009fcd0t__NSSetUnsignedIntValueAndNotify
0013e470t__NSSetUnsignedLongLongValueAndNotify
0009fc00t__NSSetUnsignedLongValueAndNotify
0013e620t__NSSetUnsignedShortValueAndNotify
看了这表会发现一些有趣的东西。第一点你会注意到苹果对它支持的最基本类型通过不同的函数来做区分。它们只需要其中一种OC对象类型(_NSSetObjectValueAndNotify)但是又需要整个函数集来做支持。这个集合其实并不完整,因为没有关于long double和_Bool类型的函数,甚至没有连正常的指针类型也木有,如果你有一个CFTypeRef类型的属性,就要去获取?(这句没懂)。如果在多个Cocoa通用的结构体中定义了一些函数,那在此以外的地方就不会出现大量相同的定义。那就意味着原子性的KVO通州并不能对所有这些类型的属性都有效。
KVO is niubility,特别是还包含了原子性的通知就显得更强力。现在你已经明确了解到它的内部实现,可能在以后的debug中能给你带来帮助。
如果你想在实际项目中使用到KVO,可以看看 Key-Value Observing Done Right。
总结(他的总结好像没啥内容。。。)
网友评论