09年的一篇文章,比较深入地阐述了KVO的内部实现。
 
KVO是实现Cocoa Bindings的基础,它提供了一种方法,当某个属性改变时,相应的objects会被通知到。在其他语言中,这种观察者模式通常需要单独实现,而在Objective-C中,通常无须增加额外代码即可使用。
 
概览
这是怎么实现的呢?其实这都是通过Objective-C强大的运行时(runtime)实现的。当你第一次观察某个object时,runtime会创建一个新的继承原先class的subclass。在这个新的class中,它重写了所有被观察的key,然后将object的isa指针指向新创建的class(这个指针告诉Objective-C运行时某个object到底是哪种类型的object)。所以object神奇地变成了新的子类的实例。
 
这些被重写的方法实现了如何通知观察者们。当改变一个key时,会触发setKey方法,但这个方法被重写了,并且在内部添加了发送通知机制。(当然也可以不走setXXX方法,比如直接修改iVar,但不推荐这么做)。
 
有意思的是:苹果不希望这个机制暴露在外部。除了setters,这个动态生成的子类同时也重写了-class方法,依旧返回原先的class!如果不仔细看的话,被KVO过的object看起来和原先的object没什么两样。
 
深入探究
下面来看看这些是如何实现的。我写了个程序来演示隐藏在KVO背后的机制。
  1. // gcc -o kvoexplorer -framework Foundation kvoexplorer.m
  2. #import <Foundation/Foundation.h>
  3. #import <objc/runtime.h>
  4. @interface TestClass : NSObject
  5. {
  6. int x;
  7. int y;
  8. int z;
  9. }
  10. @property int x;
  11. @property int y;
  12. @property int z;
  13. @end
  14. @implementation TestClass
  15. @synthesize x, y, z;
  16. @end
  17. static NSArray *ClassMethodNames(Class c)
  18. {
  19. NSMutableArray *array = [NSMutableArray array];
  20. unsigned int methodCount = 0;
  21. Method *methodList = class_copyMethodList(c, &methodCount);
  22. unsigned int i;
  23. for(i = 0; i < methodCount; i++)
  24. [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
  25. free(methodList);
  26. return array;
  27. }
  28. static void PrintDescription(NSString *name, id obj)
  29. {
  30. NSString *str = [NSString stringWithFormat:
  31. @"%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
  32. name,
  33. obj,
  34. class_getName([obj class]),
  35. class_getName(obj->isa),
  36. [ClassMethodNames(obj->isa) componentsJoinedByString:@", "]];
  37. printf("%s\n", [str UTF8String]);
  38. }
  39. int main(int argc, char **argv)
  40. {
  41. [NSAutoreleasePool new];
  42. TestClass *x = [[TestClass alloc] init];
  43. TestClass *y = [[TestClass alloc] init];
  44. TestClass *xy = [[TestClass alloc] init];
  45. TestClass *control = [[TestClass alloc] init];
  46. [x addObserver:x forKeyPath:@"x" options:0 context:NULL];
  47. [xy addObserver:xy forKeyPath:@"x" options:0 context:NULL];
  48. [y addObserver:y forKeyPath:@"y" options:0 context:NULL];
  49. [xy addObserver:xy forKeyPath:@"y" options:0 context:NULL];
  50. PrintDescription(@"control", control);
  51. PrintDescription(@"x", x);
  52. PrintDescription(@"y", y);
  53. PrintDescription(@"xy", xy);
  54. printf("Using NSObject methods, normal setX: is %p, overridden setX: is %p\n",
  55. [control methodForSelector:@selector(setX:)],
  56. [x methodForSelector:@selector(setX:)]);
  57. printf("Using libobjc functions, normal setX: is %p, overridden setX: is %p\n",
  58. method_getImplementation(class_getInstanceMethod(object_getClass(control),
  59. @selector(setX:))),
  60. method_getImplementation(class_getInstanceMethod(object_getClass(x),
  61. @selector(setX:))));
  62. return 0;
  63. }
 
我们从头到尾细细看来。
 
首先定义了一个TestClass的类,它有3个属性。
 
然后定义了一些方便调试的方法。ClassMethodNames使用Objective-C运行时方法来遍历一个class,得到方法列表。注意,这些方法不包括父类的方法。PrintDescription打印object的所有信息,包括class信息(包括-class和通过运行时得到的class),以及这个class实现的方法。
 
然后创建了4个TestClass实例,每一个都使用了不同的观察方式。x实例有一个观察者观察xkey,y, xy也类似。为了做比较,zkey没有观察者。最后control实例没有任何观察者。
 
然后打印出4个objects的description。
 
之后继续打印被重写的setter内存地址,以及未被重写的setter的内存地址做比较。这里做了两次,是因为-methodForSelector:没能得到重写的方法。KVO试图掩盖它实际上创建了一个新的subclass这个事实!但是使用运行时的方法就原形毕露了。
 
运行代码
 
看看这段代码的输出
  1. control: <TestClass: 0x104b20>
  2. NSObject class TestClass
  3. libobjc class TestClass
  4. implements methods <setX:, x, setY:, y, setZ:, z>
  5. x: <TestClass: 0x103280>
  6. NSObject class TestClass
  7. libobjc class NSKVONotifying_TestClass
  8. implements methods <setY:, setX:, class, dealloc, _isKVOA>
  9. y: <TestClass: 0x104b00>
  10. NSObject class TestClass
  11. libobjc class NSKVONotifying_TestClass
  12. implements methods <setY:, setX:, class, dealloc, _isKVOA>
  13. xy: <TestClass: 0x104b10>
  14. NSObject class TestClass
  15. libobjc class NSKVONotifying_TestClass
  16. implements methods <setY:, setX:, class, dealloc, _isKVOA>
  17. Using NSObject methods, normal setX: is 0x195e, overridden setX: is 0x195e
  18. Using libobjc functions, normal setX: is 0x195e, overridden setX: is 0x96a1a550
 
首先,它输出了controlobject,没有任何问题,它的class是TestClass,并且实现了6个set/get方法。
 
然后是3个被观察的objects。注意-class仍然显示的是TestClass,使用object_getClass显示了这个object的真面目:它是NSKVONotifying_TestClass的一个实例。这个NSKVONotifying_TestClass就是动态生成的subclass!
 
注意,它是如何实现这两个被观察的setters的。你会发现,它很聪明,没有重写-setZ:,虽然它也是个setter,因为它没有被观察。同时注意到,3个实例对应的是同一个class,也就是说两个setters都被重写了,尽管其中的两个实例只观察了一个属性。这会带来一点效率上的问题,因为即使没有被观察的property也会走被重写的setter,但苹果显然觉得这比分开生成动态的subclass更好,我也觉得这是个正确的选择。
 
你会看到3个其他的方法。有之前提到过的被重写的-class方法,假装自己还是原来的class。还有-dealloc方法处理一些收尾工作。还有一个_isKVOA方法,看起来像是一个私有方法。
 
接下来,我们输出-setX:的实现。使用-methodForSelector:返回的是相同的值。因为-setX:已经在子类被重写了,这也就意味着methodForSelector:在内部实现中使用了-class,于是得到了错误的结果。
 
最后我们通过运行时得到了不同的输出结果。
 
作为一个优秀的探索者,我们进入debugger来看看这第二个方法的实现到底是怎样的:
  1. (gdb) print (IMP)0x96a1a550
  2. $1 = (IMP) 0x96a1a550 <_NSSetIntValueAndNotify>
 
看起来是一个内部方法,对Foundation使用nm -a得到一个完整的私有方法列表:
  1. 0013df80 t __NSSetBoolValueAndNotify
  2. 000a0480 t __NSSetCharValueAndNotify
  3. 0013e120 t __NSSetDoubleValueAndNotify
  4. 0013e1f0 t __NSSetFloatValueAndNotify
  5. 000e3550 t __NSSetIntValueAndNotify
  6. 0013e390 t __NSSetLongLongValueAndNotify
  7. 0013e2c0 t __NSSetLongValueAndNotify
  8. 00089df0 t __NSSetObjectValueAndNotify
  9. 0013e6f0 t __NSSetPointValueAndNotify
  10. 0013e7d0 t __NSSetRangeValueAndNotify
  11. 0013e8b0 t __NSSetRectValueAndNotify
  12. 0013e550 t __NSSetShortValueAndNotify
  13. 0008ab20 t __NSSetSizeValueAndNotify
  14. 0013e050 t __NSSetUnsignedCharValueAndNotify
  15. 0009fcd0 t __NSSetUnsignedIntValueAndNotify
  16. 0013e470 t __NSSetUnsignedLongLongValueAndNotify
  17. 0009fc00 t __NSSetUnsignedLongValueAndNotify
  18. 0013e620 t __NSSetUnsignedShortValueAndNotify
这个列表也能发现一些有趣的东西。比如苹果为每一种primitive type都写了对应的实现。Objective-C的object会用到的其实只有__NSSetObjectValueAndNotify,但需要一整套来对应剩下的,而且看起来也没有实现完全,比如long dobule或_Bool都没有。甚至没有为通用指针类型(generic pointer type)提供方法。所以,不在这个方法列表里的属性其实是不支持KVO的。
 
KVO是一个很强大的工具,有时候过于强大了,尤其是有了自动触发通知机制。现在你知道它内部是怎么实现的了,这些知识或许能帮助你更好地使用它,或在它出错时更方便调试。
 
如果你打算使用KVO,或许可以看一下我的另一篇文章Key-Value Observing Done Right
 
 
http://www.cocoachina.com/industry/20140107/7667.html

(译)KVO的内部实现的更多相关文章

  1. [转](译)KVO的内部实现

    转载自:http://www.cocoachina.com/applenews/devnews/2014/0107/7667.html   09年的一篇文章,比较深入地阐述了KVO的内部实现.   K ...

  2. KVO的内部实现原理

    kvo概述 kvo,全称Key-Value Observing,它提供了一种方法,当对象某个属性发生改变时,允许监听该属性值变化的对象可以接受到通知,然后通过kvo的方法响应一些操作. kvo实现原理 ...

  3. KVO的内部实现以及使用

    转载自:http://www.cocoachina.com/applenews/devnews/2014/0107/7667.html   KVO是实现Cocoa Bindings的基础,它提供了一种 ...

  4. 你不知道的KVO的内部实现

    通过强大的Runtime 实现.第一次观察某个Object 时,runtime 会创建一个新的继承自 object 对应Class 的 subClass.在这个新subClass 里它重写了被观察的k ...

  5. runtime/KVO等面试题

    整理中... 1.KVO内部实现原则 回答:1>KVO是基于runtime机制实现的 2>当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中 ...

  6. KVO的使用及底层实现

    1.概念 KVO(Key-Value-Observer)也就是观察者模式,是苹果提供的一套事件通知机制.允许对象监听另一个对象特定属性的改变,并在改变时接收到事件,一般继承自NSObject的对象都默 ...

  7. 使用 KVO 可能会拖慢启动速度

    问题  在某一次启动速度优化中,发现最开始的某个 runLoop 中,一个runLoop 耗时很长.发现一个 KVO 变量的初始化消耗了13ms之久,这对启动速度是不可接受了. 源码分析 用 Ins ...

  8. iOS面试题04-runtime

    runtime/KVO等面试题 1.KVO内部实现原则 回答:1>KVO是基于runtime机制实现的 2>当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个 ...

  9. iOS设计模式 —— KV0

    刨根问底KVO KVO 全称 Key-Value Observing.中文叫键值观察.KVO其实是一种观察者模式,观察者在键值改变时会得到通知,利用它可以很容易实现视图组件和数据模型的分离,当数据模型 ...

随机推荐

  1. 前端学习笔记之JavaScript

    JavaScript概述 JavaScript的历史 1992年Nombas开发出C-minus-minus(C--)的嵌入式脚本语言(最初绑定在CEnvi软件中),后将其改名ScriptEase(客 ...

  2. CentOS下 Nginx1.13.5 + PHP7.1.10 + MySQL5.7.19 源码编译安装

    一.安装Nginx ①安装依赖扩展 # yum -y install wget openssl* gcc gcc-c++ autoconf libjpeg libjpeg-devel libpng l ...

  3. P1136 迎接仪式

    P1136 迎接仪式 $O(n^{2}k)$:$f[i][k]$表示到第$i$个字符为止,交换$k$次,得到的最多子串数 那么枚举位置$j$,状态可以从$f[j][k-1]+1$转移过来 $O(nk^ ...

  4. 2018-2019-1 20189215 《Linux内核原理与分析》第九周作业

    进程的切换和系统 <庖丁解牛>第八章书本知识总结 进程调度的时机都与中断相关,中断是程序执行过程中的强制性转移,转移到操作系统内核相应的处理程序. 软中断也叫异常,分为故障.退出和陷阱(自 ...

  5. Bzoj 4371: [IOI2015]sorting排序 二分

    题目 似乎很久没写题解了... 这题是校里胡策的时候的题,比赛因为评测机有点慢+自己代码常数大没快读...被卡t了,但是bzoj上还是A了的...,因为bzoj时限比较宽可以不卡常. 题解: 首先可以 ...

  6. windows下hadoop安装配置(转载)

    Windows平台安装配置Hadoop 步骤: 1. JDK安装(不会的戳这) 2. 下载hadoop2.5.2.tar.gz,或者自行去百度下载. 3. 下载hadooponwindows-mast ...

  7. css3 属性——calc()

    其实在之前学习CSS3的时候,我并没有注意到有calc()这个属性,后来在看一个大牛的代码的时候看到了这个,然后就引发了后来的一系列的查找.学习,以及这篇博客的诞生.好了,废话不多说了,来干正事. 一 ...

  8. 大白菜的装机U盘真不错

    今天用大白菜制作了个启动U盘,然后在里面,把[电脑公司]的ghost文件拷贝进去. 重新安装WinXP成功. 注意: 1)直接用[电脑公司]的ISO文件,用Win32DiskImage写到U盘,不知为 ...

  9. Spring学习笔记1——基础知识

    1.在java开发领域,Spring相对于EJB来说是一种轻量级的,非侵入性的Java开发框架,曾经有两本很畅销的书<Expert one-on-one J2EE Design and Deve ...

  10. UVALive-3487 Duopoly(最小割)

    题目大意:有两家公司都想向政府申请某些资源的使用权,并且他们都提供了一些申请列表,列表中含有申请费用和资源种类,同一家公司的申请列表之间不含有重复的资源.政府只可以完整地接受和拒绝谋一份申请列表,问政 ...