KVO-KVC的原理探究 - KVO篇

关于KVO的探究

KVO的基本使用

创建Person类,添加属性age:


@interface Person : NSObject

@property (nonatomic, assign) NSInteger age;

@end

在ViewController中添加属性@property (nonatomic, strong) Person * person1;

实例化并添加KVO观察age属性:


self.person1 = [[Person alloc] init];    

self.person1.age = 1;

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

添加观察监听回调并打印:


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {

 NSLog(@"被监听的 %@ 的值 %@ 改变为 %@", object, keyPath, change);

}

此时准备工作完成,当点击view时就会修改age的值,并且回调打印出监听的结果,这里在ViewController的touchedBegan中修改值:


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

 self.person.age = 11;

}

记得在最后移除键值观察


- (void)dealloc {

 [self.person1 removeObserver:self forKeyPath:@"age"];

}

以上为KVO的基本使用。

关于KVO的疑问和分析

再次添加属性 @property (nonatomic, strong) Person * person2;

实例化person2,在touchedBegan方法中修改值但是不添加KVO:


self.person2 = [[Person alloc] init];

self.person2.age = 2;

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

 self.person.age = 11;

 self.person1.age = 22;

}

点击view可以看到打印台的日志为:


2018-07-17 14:09:26.944619+0800 KVO-KVC[36344:935709] 被监听的 <Person: 0x6040000106d0> 的值 age 改变为 {

 kind = 1;

 new = 11;

 old = 1;

}

这时就可以思考都是修改age属性值,为什么person1会有回调而person2没有,修改的本质都是调用age的set方法。猜想person1和person2的set方法实现可能不一样,但是实例方法都是存放在class中的,set方法应该是一样的才对,在touchesBegan处打断点,然后直接查看person1和person2的isa指针,看看person1和person2的class是否一样:


(lldb) p self.person1.isa

(Class) $0 = NSKVONotifying_Person

 Fix-it applied, fixed expression was: 

 self.person1->isa

(lldb) p self.person2.isa

(Class) $1 = Person

 Fix-it applied, fixed expression was: 

 self.person2->isa

可以看到person1的class为 NSKVONotifying_Person person2的class为 Person ,isa指针指向的就是instance的class,但是为什么person1和person2会不一样呢?我们在添加键值观察之前和之后分别打印person的类型:


NSLog(@"添加前 person1 : %@ person2 : %@", object_getClass(self.person1), object_getClass(self.person2));

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

 [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"添加后 person1 : %@ person2 : %@", object_getClass(self.person1), object_getClass(self.person2));

打印的结果为


2018-07-17 14:40:59.918227+0800 KVO-KVC[37038:970983] 添加前 person1 : Person person2 : Person

2018-07-17 14:40:59.918636+0800 KVO-KVC[37038:970983] 添加后 person1 : NSKVONotifying_Person person2 : Person

可以看到添加键值观察之后person1的isa指针指向确实被修改了,指向了 NSKVONotifying_Person 类,结合上面的猜想,会不会是 NSKVONotifying_Person 这个类重新实现了person1的 setAge: ,否则怎么会和person2不一样呢?

我们来验证一下,通过 methodForSelector: 来获取 setAge: 的实现:


NSLog(@"添加前 person1 : %p person2 : %p",

 [self.person1 methodForSelector:@selector(setAge:)],

 [self.person2 methodForSelector:@selector(setAge:)]);

NSLog(@"添加后 person1 : %p person2 : %p",

 [self.person1 methodForSelector:@selector(setAge:)],

 [self.person2 methodForSelector:@selector(setAge:)]);

打印的结果为


2018-07-17 14:46:56.489956+0800 KVO-KVC[37183:978368] 添加前 person1 : 0x102493570 person2 : 0x102493570

2018-07-17 14:46:56.490699+0800 KVO-KVC[37183:978368] 添加后 person1 : 0x1027d9bf4 person2 : 0x102493570

我们知道instance的方法、属性、协议等信息都存在与class中,所以当person1和person2调用 setAge: 时得到的地址应该是一样的,但是在添加键值观察之后person1的调用方法地址改变了,为什么会改变呢?让我们来看看这两个地址的IMP,在添加键值观察之后断点,直接查看两个地址的IMP:


(lldb) p (IMP)0x100a43570

(IMP) $0 = 0x0000000100a43570 (KVO-KVC -[Person setAge:] at Person.m:13)

(lldb) p (IMP)0x100d89bf4

(IMP) $1 = 0x0000000100d89bf4 (Foundation _NSSetLongLongValueAndNotify)

可以看到添加键值观察之后调用 setAge: 方法其实就是调用了 Foundation _NSSetLongLongValueAndNotify

由此可以猜测在添加键值观察之后person1的isa指向了新生成的类 NSKVONotifying_PersonNSKVONotifying_Person 可能继承自 Person 类,并且重写了 setAge: 方法,伪代码如下:


- (void)setAge:(NSInteger)age {

 _NSSetLongLongValueAndNotify();

}

void _NSSetLongLongValueAndNotify() {

 [self willChangeValueForKey:@"age"];

 [super setAge:age];

 [self didChangeValueForKey:@"age"];

}

- (void)didChangeValueForKey:(NSString *)key {

 [observer observeValueForKeyPath:key ofObject:self change:opetions context:nil];

}

综上我们的猜想KVO的实现:instance添加键值观察之后isa指针会被修改为指向 NSKVONotifying_PersonNSKVONotifying_Person 继承自 Person 并且重写了 setAge: 方法,方法实现如上。
在这里就有了那道最经典的面试题:如何手动实现KVO,我们只需要在修改值的时候替换 _NSSetLongLongValueAndNotify 方法里面的 [super setAge:age]; 就好了。

KVO内部实现窥探

由上我们猜测出了KVO的实现原理,下面我们来继续探索一下KVO内部的实现。

我们分别在添加KVO前后打印person1和person2的class,这次我们用两种方式:


NSLog(@"添加前 person1 : %@ -- %@  person2 : %@ -- %@", [self.person1 class], object_getClass(self.person1), [self.person2 class], object_getClass(self.person2));

NSLog(@"添加后 person1 : %@ -- %@  person2 : %@ -- %@", [self.person1 class], object_getClass(self.person1), [self.person2 class], object_getClass(self.person2));

打印出的结果为:


2018-07-19 11:05:50.553735+0800 KVO-KVC[40616:2560144] 添加前 person1 : Person -- Person  person2 : Person -- Person

2018-07-19 11:05:52.772905+0800 KVO-KVC[40616:2560144] 添加后 person1 : Person -- NSKVONotifying_Person  person2 : Person -- Person

可以看到我们通常用来获取class的方法在添加前后结果都是 Person ,通过runtime API获取到的class不相同,怎么回事呢?我们先来看一下苹果官方runtime的源码 这里,当然官方的编译是失败,要想调试runtime的请看 这里

我们来分析一下源码:


class方法:

+ (Class)class {

 return self;

}

- (Class)class {

 return object_getClass(self);

}

runtime object_getClass方法:

Class object_getClass(id obj) {

 if (obj) return obj->getIsa();

 else return Nil;

}

class 的类方法或者实例方法最终返回的都是class的self,而 object_getClass 方法返回的是obj的isa指针,所以通过 object_getClass 获取的才是当前obj的真正class,所以在添加KVO之后person1的isa指针确确实实是被修改了。

我们再来看一下捕捉到的 NSKVONotifying_Person 到底是个什么鬼?

先来看一下 NSKVONotifying_Person 的meta-class:


NSLog(@"元类对象 person : %@ person1 : %@",

 object_getClass(object_getClass(self.person1)),

 object_getClass(object_getClass(self.person2)));

打印结果:

2018-07-19 11:39:30.210378+0800 KVO-KVC[41164:2599225] 元类对象 person : NSKVONotifying_Person person1 : Person

NSKVONotifying_Person 的meta-class为 NSKVONotifying_Person

在添加KVO之后打住断点,借用 DLIntrospection 再来查看一下此时class里面方法都有什么:


(lldb) po [[self.person1 class] instanceMethods]

<__NSArrayI 0x60400023daa0>(

- (void)setAge:(q)arg0 ,

- (q)age

)

(lldb) po [object_getClass(self.person1) instanceMethods]

<__NSArrayI 0x60400025fb30>(

- (void)setAge:(q)arg0 ,

- (class)class,

- (void)dealloc,

- (BOOL)_isKVOA

)

结果可以看到 NSKVONotifying_Person 重写了 setAge: 方法,并且还有其他的三个方法,可证上面的猜想确实没错,NSKVONotifying_Person重写了 setAge: 方法,但是还有一个上面的猜想没有验证,那就是 NSKVONotifying_Person 的superClass到底是谁?

类似isa指针的方式,我们断点直接打印:


(lldb) po self.person1.superclass

NSObject

(lldb) po self.person2.superclass

NSObject

咦~~~ 等等,这跟我们猜测的不一样啊,怎么superclass都是NSObject呢?那我们的猜测是不是都错了?

为了看看superClass里面到底是什么下面我们请出 clang 大神:

clang -rewrite-objc Person.m

可以看出编译完成后Person类被编译成了这样:


struct NSObject_IMPL {

 Class isa;

};

struct Person_IMPL {

 struct NSObject_IMPL NSObject_IVARS;

 NSInteger _age;

};

结合runtime源码分析,Class为 typedef struct objc_class *Class; 类型的结构体,再看下结构体里面的结构:


struct objc_object {

private:

 isa_t isa;

 ···

}

struct objc_class : objc_object {

 // Class ISA;

 Class superclass;

 cache_t cache;

 class_data_bits_t bits;

 ···

}

里面确实有superclass,仿照runtime的结构我们自己来创建一个类似的结构体:


struct XFPerson_IMPL {

 Class isa;

 Class super_Class;

 NSInteger _age;

};

用我们自己创建的结构体来接收 NSKVONotifying_Person ,看看他的superclass到底是什么类型:


struct XFPerson_IMPL * xfPerson1 = (__bridge struct XFPerson_IMPL *)(object_getClass(self.person1));

struct XFPerson_IMPL * xfPerson2 = (__bridge struct XFPerson_IMPL *)(object_getClass(self.person2));

NSLog(@"person1--- %@", xfPerson1->super_Class);

NSLog(@"person2--- %@", xfPerson2->super_Class);

打印结果:

2018-07-19 14:05:38.855549+0800 KVO-KVC[43578:2717734] person1--- Person

2018-07-19 14:05:38.855658+0800 KVO-KVC[43578:2717734] person2--- NSObject

结果可见是符合我们的猜想的,NSKVONotifying_Person 确实是Person的子类,但是为什么上面直接打印instance的superclass却都是NSObject呢?

回过头来看一下上面我们找到的 NSKVONotifying_Person 除了 setAge: 还有三个方法,其中就有class方法,我们已经知道runtime的class的实现,class返回的就是self,而通过 [self.person1 class] 得到的是 Person ,这就证明了 NSKVONotifying_Person 重写了class方法,并且返回的是 Person 类,通过源码查看runtime的superclass方法的实现:


+ (Class)superclass {

 return self->superclass;

}

- (Class)superclass {

 return [self class]->superclass;

}

就是先通过class方法找到class,然后在根据class找到superclass,所以前面直接通过 self.person1.superclass 找到的是 Person,因为此时的class方法返回已经被修改了。

苹果大大可能是因为整个事件中 NSKVONotifying_Person 是个人畜无害的东西,对于开发者使用KVO是可以不用知道的,所以用这种方式来骗骗开发者,真不容易,还好最近看 白夜追凶 看的整个人都比较有耐心了就是要找到真相,哈(不)哈(要)哈(脸)😁。

再看看看其他的两个方法,dealloc 方法可能就是做一些销毁现场的事情,毕竟中间动态创建了 NSKVONotifying_Person ,不用了一定要销毁,而 _isKVOA 返回的一定是 YES ,表示当前确实是在用KVO,到此关于KVO的黑科技已经探究明白了,好了,打完收工,接着去看两集 白夜追凶, 哈哈哈。

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

推荐阅读更多精彩内容

  • 面试问题: iOS用什么方式实现对一个对象的KVO? 如何手动触发KVO? KVO简介 KVO就是键值观测。有时候...
    雪山飞狐_91ae阅读 4,522评论 10 36
  • 问题 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 如何手动触发KVO ? 首先需要了解KVO...
    hjltony阅读 572评论 0 2
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,021评论 8 265
  • 1、KVO的基本使用 定义:KVO的全称是Key-Value-Observing,俗称“键值监听”,可以用于监听某...
    Jerky_Guo阅读 1,686评论 0 5
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32