Key-Value Observing(KVO)的工作原理

它是什么?

大多数读者可能已经知道了这一点,但只是为了快速回顾一下:KVO是cocoa绑定的基础技术,它为其他对象的属性更改时通知对象提供了一种方法。一个对象观察另一个对象的键。当观察对象更改该键的值时,观察者会收到通知。很简单吧?棘手的部分是,KVO的操作通常不需要在被观察物体上进行编码。

概述

那么,在被观察对象中不需要任何代码的情况下,它是如何工作的呢?好吧,这一切都是通过Objective-C运行时的力量来实现的。当您第一次观察特定类的对象时,KVO基础结构会在运行时创建一个全新的类,该类将您的类作为子类。在该新类中,它会覆盖所有观察到的键的set方法。然后,它切换出对象的isa指针(该指针告诉Objective-C运行时特定的内存块实际上是哪种对象),从而使对象神奇地成为该新类的实例。

被覆盖的方法是它如何实际完成通知观察者的工作。逻辑上,对键的更改必须通过该键的set方法进行。它重写该set方法,以便它可以拦截它,并在调用它时将通知发布到观察者。 (当然,如果直接修改实例变量,则无需通过set方法就可以进行修改。KVO要求兼容类不能这样做,或者必须在手动通知调用中包装直接ivar访问。)

但是,它变得更加棘手:Apple确实不希望这种机器暴露出来。除了setter之外,动态子类还重写-class方法来欺骗您并返回原始类!如果您看起来不太近,则KVO突变的对象看起来就像它们的未观察对象。

深入挖掘

聊够了,让我们实际看看所有这些是如何工作的。我写了一个程序来说明KVO背后的原理。因为动态KVO子类试图隐藏其自身的存在,所以我主要使用Objective-C运行时调用来获取我们正在寻找的信息。

下边是代码:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
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:<TestClass: 0x600002e21b40>
    NSObject class TestClass
    libobjc class TestClass
    implements methods <setZ:,x,setX:,y,setY:,z>
x:<TestClass: 0x600002e21ba0>
    NSObject class TestClass
    libobjc class NSKVONotifying_TestClass
    implements methods <setY:,setX:,class,dealloc,_isKVOA>
y:<TestClass: 0x600002e21b80>
    NSObject class TestClass
    libobjc class NSKVONotifying_TestClass
    implements methods <setY:,setX:,class,dealloc,_isKVOA>
xy:<TestClass: 0x600002e21b60>
    NSObject class TestClass
    libobjc class NSKVONotifying_TestClass
    implements methods <setY:,setX:,class,dealloc,_isKVOA>
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的文章。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353