[关闭]
@blueGhost 2017-06-30T17:16:51.000000Z 字数 6329 阅读 1441

runtime04-KVO-internal-implementation

iOS


参考文献:
Apple官方文档 Key-Value Observing Programming Guide
KVO internal implementation
NShisper Key-Value Observing tutorial


一、KVO的内部实现

1.要点:
  1. KVO内部通过isa-swizzling实现
  2. 当对某个对象注册KVO,则被观察对象的isa指针会指向一个新的class(我们姑且叫做“中间人class”intermediate class)
  3. 这个新的class已经不是原来对象的class,充其量只是一个中间代理,因此apple不推荐直接通过isa指针取出对象的class,而是推荐用class方法
  4. 代理class重写了property的setter 等相关方法的IMP实现,以便即使通知观察者property的变动(变量初始化的通知是怎么弄的还得查纠)
  5. 代理class和原来真正的class之间有某种关联并且对外隐藏,使得原来class维持的信息依旧有效(比如继承链)
  6. 为什么不直接swizzle property的setter等方法,而是swizzle isa呢,因为直接交换set/get,会导致所有该class的instances都加入了这些新逻辑

二、代码验证:

1.上代码
  1. #import <Foundation/Foundation.h>
  2. #import "FFPerson.h"
  3. #import <objc/runtime.h>
  4. #import <objc/message.h>
  5. void printChar(char *ch);
  6. NSArray * ClassMethodNames(Class c);
  7. void printDes(NSString *name, id obj);
  8. int main(int argc, const char * argv[]) {
  9. @autoreleasepool {
  10. // insert code here...
  11. FFPerson *p1 = [[FFPerson alloc]init];
  12. FFPerson *p2 = [[FFPerson alloc]init];
  13. FFPerson *p3 = [[FFPerson alloc]init];
  14. [p2 addObserver:p2 forKeyPath:@"name" options:0 context:NULL];
  15. [p3 addObserver:p3 forKeyPath:@"info" options:0 context:NULL];
  16. printDes(@"p1", p1);
  17. printDes(@"p2", p2);
  18. printDes(@"p3", p3);
  19. // obj->isa,isa指针指向obj对象的class,class包含了obj对象关于类的信息和实现
  20. // object_getClass(obj)相当于旧版runtime的obj->isa,目前版本的isa已经对外不可见了
  21. printf("Using libobjc setName, p1: %p, p2: %p, p3: %p\n",
  22. method_getImplementation(class_getInstanceMethod(object_getClass(p1),
  23. @selector(setName:))),
  24. method_getImplementation(class_getInstanceMethod(object_getClass(p2),
  25. @selector(setName:))),
  26. method_getImplementation(class_getInstanceMethod(object_getClass(p3),
  27. @selector(setName:))));
  28. printf("Using libobjc name, p1: %p, p2: %p, p3: %p\n",
  29. method_getImplementation(class_getInstanceMethod(object_getClass(p1),
  30. @selector(name))),
  31. method_getImplementation(class_getInstanceMethod(object_getClass(p2),
  32. @selector(name))),
  33. method_getImplementation(class_getInstanceMethod(object_getClass(p3),
  34. @selector(name))));
  35. printf("Using libobjc setInfo, p1: %p, p2: %p, p3: %p\n",
  36. method_getImplementation(class_getInstanceMethod(object_getClass(p1),
  37. @selector(setInfo:))),
  38. method_getImplementation(class_getInstanceMethod(object_getClass(p2),
  39. @selector(setInfo:))),
  40. method_getImplementation(class_getInstanceMethod(object_getClass(p3),
  41. @selector(setInfo:))));
  42. printf("Using libobjc info, p1: %p, p2: %p, p3: %p\n",
  43. method_getImplementation(class_getInstanceMethod(object_getClass(p1),
  44. @selector(info))),
  45. method_getImplementation(class_getInstanceMethod(object_getClass(p2),
  46. @selector(info))),
  47. method_getImplementation(class_getInstanceMethod(object_getClass(p3),
  48. @selector(info))));
  49. }
  50. return 0;
  51. }
  52. void printDes(NSString *name, id obj){
  53. NSString *des = [NSString stringWithFormat: @"%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@> \n\tsuperClass %s",
  54. name,
  55. obj,
  56. class_getName([obj class]),
  57. class_getName(object_getClass(obj)),
  58. [ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "],
  59. class_getName(class_getSuperclass(object_getClass(obj)))];
  60. printf("%s\n", [des UTF8String]);
  61. }
  62. NSArray * ClassMethodNames(Class c){
  63. NSMutableArray *arr = [NSMutableArray array];
  64. uint methodCount;
  65. Method *methodList = class_copyMethodList(c, &methodCount);
  66. for (uint i = 0; i < methodCount; i++) {
  67. [arr addObject:NSStringFromSelector(method_getName(methodList[i]))];
  68. }
  69. free(methodList);
  70. return arr;
  71. }

FFPerson的内容:

  1. //FFPerson.h
  2. @interface FFPerson : NSObject
  3. @property(nonatomic, copy)NSString *name;
  4. @property(nonatomic, strong)NSDictionary *info;
  5. @end
  6. //FFPerson.m
  7. @implementation FFPerson
  8. @end

打印结果:

  1. p1: <FFPerson: 0x1003017d0>
  2. NSObject class FFPerson
  3. libobjc class FFPerson
  4. implements methods <setInfo:, .cxx_destruct, info, name, setName:>
  5. superClass NSObject
  6. p2: <FFPerson: 0x1003030f0>
  7. NSObject class FFPerson
  8. libobjc class NSKVONotifying_FFPerson
  9. implements methods <setInfo:, setName:, class, dealloc, _isKVOA>
  10. superClass FFPerson
  11. p3: <FFPerson: 0x100303110>
  12. NSObject class FFPerson
  13. libobjc class NSKVONotifying_FFPerson
  14. implements methods <setInfo:, setName:, class, dealloc, _isKVOA>
  15. superClass FFPerson
  16. Using libobjc setName, p1: 0x1000012e0, p2: 0x7fff879f0957, p3: 0x7fff879f0957
  17. Using libobjc name, p1: 0x1000012c0, p2: 0x1000012c0, p3: 0x1000012c0
  18. Using libobjc setInfo, p1: 0x100001340, p2: 0x7fff879f0957, p3: 0x7fff879f0957
  19. Using libobjc info, p1: 0x100001320, p2: 0x100001320, p3: 0x100001320
2.分析

1.证明obj的isa被swizzle了:

  1. 苹果的class方法([obj class])对外还是返回原始的class,而不是swizzle后的class(下面第二行)
  2. 通过runtime API,object_getClass(obj),直接通过isa取出class,发现isa指向的是NSKVONotifying_FFPerson,终于原形毕露了,也证明了isa被swizzled了。
  3. 这里说明一下object_getClass(obj)方法的效果和obj->isa效果一致(起码目前看起来是,后续是否这样取决于苹果爸爸的实现了),这是由于新版本runtime,苹果爸爸不再对外开放isa指针的访问了,而是封装起来了。
  1. p2: <FFPerson: 0x1003030f0>
  2. NSObject class FFPerson
  3. libobjc class NSKVONotifying_FFPerson

2.证明苹果通过使代理的class继承自原始的class来保证原有的继承链和相关的类信息,具体到本例,就是NSKVONotifying_FFPerson的superClass是FFPerson
第三行打印出isa指向的class为NSKVONotifying_FFPerson
第四行打印出NSKVONotifying_FFPerson的方法列表
第五行打印出NSKVONotifying_FFPerson的superClass

  1. p2: <FFPerson: 0x1003030f0>
  2. NSObject class FFPerson
  3. libobjc class NSKVONotifying_FFPerson
  4. implements methods <setInfo:, setName:, class, dealloc, _isKVOA>
  5. superClass FFPerson

3.证明在代理类中,setter方法的IMP实现改变了
第二、四行,打印出getter,发现getter的IMP并没有被改变,因为没有这个必要。
第一、三行,p2,p3,不管是setName还是setInfo,都指向了同一个IMP实现,这说明,有一个统一的IMP处理KVO(实际上,是针对每个支持KVO的变量的类型来区分的,相同的变量类型具有相同的IMP)

  1. Using libobjc setName, p1: 0x1000012e0, p2: 0x7fff879f0957, p3: 0x7fff879f0957
  2. Using libobjc name, p1: 0x1000012c0, p2: 0x1000012c0, p3: 0x1000012c0
  3. Using libobjc setInfo, p1: 0x100001340, p2: 0x7fff879f0957, p3: 0x7fff879f0957
  4. Using libobjc info, p1: 0x100001320, p2: 0x100001320, p3: 0x100001320

4.基于3,那么新的IMP实现是什么?我们调试一下打印出来,它是_NSSetObjectValueAndNotify,在Foundation.framework中。

  1. (lldb) expr (IMP) 0x7fff879f0957
  2. (IMP) $0 = 0x00007fff879f0957 (Foundation`_NSSetObjectValueAndNotify)

如果对Foundation包执行“nm -a”命令,可以梳理出所有方法,同时聪明的你发现了,苹果KVO能够支持的property类型都在下面的方法对应上了,除了下面的类型,其它类型不支持KVO。
同时我们知道了,由于name/info属性,都是Object类型(NSString/NSDictionary),所以打印出来的IMP都是0x7fff879f0957,都指向了_NSSetObjectValueAndNotify。假如info换成int类型,你可以实验一下,地址就不同了。

  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
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注