(译)KVO内部实现

翻译自: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

总结(他的总结好像没啥内容。。。)

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

推荐阅读更多精彩内容