它是什么?
大多数读者可能已经知道了这一点,但只是为了快速回顾一下:KVO是cocoa绑定的基础技术,它为其他对象的属性更改时通知对象提供了一种方法。一个对象观察另一个对象的键。当观察对象更改该键的值时,观察者会收到通知。很简单吧?棘手的部分是,KVO的操作通常不需要在被观察物体上进行编码。
概述
那么,在被观察对象中不需要任何代码的情况下,它是如何工作的呢?好吧,这一切都是通过Objective-C运行时的力量来实现的。当您第一次观察特定类的对象时,KVO基础结构会在运行时创建一个全新的类,该类将您的类作为子类。在该新类中,它会覆盖所有观察到的键的set方法。然后,它切换出对象的isa指针(该指针告诉Objective-C运行时特定的内存块实际上是哪种对象),从而使对象神奇地成为该新类的实例。
被覆盖的方法是它如何实际完成通知观察者的工作。逻辑上,对键的更改必须通过该键的set方法进行。它重写该set方法,以便它可以拦截它,并在调用它时将通知发布到观察者。 (当然,如果直接修改实例变量,则无需通过set方法就可以进行修改。KVO要求兼容类不能这样做,或者必须在手动通知调用中包装直接ivar访问。)
但是,它变得更加棘手:Apple确实不希望这种机器暴露出来。除了setter之外,动态子类还重写-class方法来欺骗您并返回原始类!如果您看起来不太近,则KVO突变的对象看起来就像它们的未观察对象。
深入挖掘
聊够了,让我们实际看看所有这些是如何工作的。我写了一个程序来说明KVO背后的原理。因为动态KVO子类试图隐藏其自身的存在,所以我主要使用Objective-C运行时调用来获取我们正在寻找的信息。
下边是代码:
#import
#import
NS_ASSUME_NONNULL_BEGIN
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(object_getClass(obj)),
[ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@","]];
printf("%s\n",[str UTF8String]);
}
@interface TestClass : NSObject
{
int x;
int y;
int z;
}
@property int x;
@property int y;
@property int z;
@end
NS_ASSUME_NONNULL_END
#import "TestClass.h"
@implementation TestClass
@synthesize x, y, z;
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
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 UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
首先,我们定义一个名为TestClass的类,它具有三个属性。 (KVO也可以在非@属性键上使用,但这是定义一对setter和getter的最简单方法。)
接下来,我们定义一对实用程序函数。 ClassMethodNames使用Objective-C运行时函数遍历一个类并获取其实现的所有方法的列表。请注意,它只能直接在该类中实现方法,而不能在超类中实现。 PrintDescription打印传递给它的对象的完整描述,显示通过-class方法以及通过Objective-C运行时函数获得的对象的类,以及在该类上实现的方法。
然后,我们创建四个TestClass实例,每个实例将以不同的方式进行观察。 x实例在其x键上将具有一个观察者,与y相似,并且xy将同时获得两者。出于比较目的,不注意z键。最后,控制实例可作为实验的控制,根本不会被观察到。
接下来,我们打印出所有四个对象的描述。
之后,我们将更深入地研究重写的setter,并在控制对象和观察对象上打印出-setX:方法的实现地址,以进行比较。我们执行了两次,因为使用-methodForSelector:无法显示替代。 KVO试图隐藏动态子类的尝试甚至使用这种技术也隐藏了被覆盖的方法!但是,当然,使用Objective-C运行时函数可以提供适当的结果。
运行代码,输出以下结果:
control:
NSObject class TestClass
libobjc class TestClass
implements methods
x:
NSObject class TestClass
libobjc class NSKVONotifying_TestClass
implements methods
y:
NSObject class TestClass
libobjc class NSKVONotifying_TestClass
implements methods
xy:
NSObject class TestClass
libobjc class NSKVONotifying_TestClass
implements methods
Using NSObject methods, normal setX: is 0x101e236a0, overridden setX: is 0x1020f60eb
Using libobjc functions, normal setX: is 0x101e236a0, overridden setX: is 0x1020f60eb
首先,它打印我们的控制对象。不出所料,它的类是TestClass,它实现了我们根据类的属性综合的六个方法。
接下来,它将打印三个观察到的对象。请注意,虽然-class仍显示TestClass,但使用object_getClass可以显示此对象的真实外观:它是NSKVONotifying_TestClass的实例。有您的动态子类!
注意它如何实现两个观察到的setter。这很有趣,因为您会注意到它很聪明,不会覆盖-setZ:即使这也是一个setter,因为没有人注意到它。大概如果我们还要向z添加一个观察者,则NSKVONotifying_TestClass会突然产生-setZ:覆盖。但也请注意,这三个实例的类均相同,这意味着它们都覆盖了两个设置器,即使其中两个只具有一个观察到的属性。由于即使对于未观察的属性也要通过观察到的设置器,因此这会花费一定的效率,但是Apple显然认为,如果每个对象都观察到不同的键集,则最好不要传播动态子类,我认为是正确的选择。
您还将注意到其他三种方法。如前所述,有一个重写的-class方法,该方法试图隐藏此动态子类的存在。有一个-dealloc方法来处理清理。还有一个神秘的-_isKVOA方法,它看起来像是一种私有方法,Apple代码可以用来确定对象是否受到此动态子类的约束。
接下来,我们打印出-setX:的实现。使用-methodForSelector:为两者返回相同的值。由于动态子类中没有此方法的替代,因此这必须意味着-methodForSelector:将-class用作其内部工作的一部分,并因此得到错误的答案。
因此,我们当然完全绕开了这一步,并使用Objective-C运行时来打印实现,在这里我们可以看到区别。原始版本与-methodForSelector:(当然应该)相同,但是第二个则完全不同。
KVO是一项强大的技术,有时会有些强大,尤其是在涉及自动通知时。现在,您确切地知道了它在内部的所有工作原理,这些知识可以帮助您决定如何使用它或在出现错误时对其进行调试。
如果您打算在自己的应用程序中使用KVO,则可能需要查看我有关Key-Value Observing Done Right的文章。