IOS-KVO实现的原理和本质

面试题

问题一:iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

问题二:如何手动触发KVO?(就算没有人修改age值,也想触发监听方法observeValueForKeyPath)

问题三:直接修改成员变量会触发KVO吗?

KVO的基本使用

KVO我们经常使用,KVO的全称是: Key-Value Observing ,俗称"键值监听",可以用来监听某个对象的属性改变.

为了接下来的研究,我们创建一个MJPerson类,类中添加一个age属性,然后在ViewController中创建person1,person2两个属性,给person1添加观察者,如下:

KVO.png
@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    self.person1.height = 11;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    self.person2.height = 22;
    
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    [self.person1 addObserver:self forKeyPath:@"height" options:options context:@"456"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 20;
    self.person2.age = 20;
    
    self.person1.height = 30;
    self.person2.height = 30;
}
//移除监听:
- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
    [self.person1 removeObserver:self forKeyPath:@"height"];
}

// 当监听对象的属性值发生改变时,就会调用
/**
 当监听对象的属性值发生改变时,就会调用
 @param keyPath 监听的KeyPath
 @param object 被监听的对象
 @param change 改变
 @param context 监听时传入的context
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

@end

RUN>

================打印结果================
2021-04-16 14:54:13.669370+0800 Interview01[5093:145779] 监听到<MJPerson: 0x60000058c5a0>的age属性值改变了 - {
    kind = 1;
    new = 20;
    old = 1;
} - 123
2021-04-16 14:54:13.669516+0800 Interview01[5093:145779] 监听到<MJPerson: 0x60000058c5a0>的height属性值改变了 - {
    kind = 1;
    new = 30;
    old = 11;
} 

KVO底层是怎么实现的

为了探究KVO的底层是怎么实现的,我们创建person1和person2,其中person1添加监听,person2不添加监听,代码如下:

#import "ViewController.h"
#import "MJPerson.h"

@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    self.person1.age = 21;
//    self.person2.age = 22;
    
    // NSKVONotifying_MJPerson是使用Runtime动态创建的一个类,是MJPerson的子类
    //如果你自己写了这个类,就会报动态生成失败
    //KVO效率没代理高,因为代理是直接调用,KVO还要动态生成一个类
    
    // self.person1.isa == NSKVONotifying_MJPerson
    [self.person1 setAge:21];
    
    // self.person2.isa = MJPerson
    [self.person2 setAge:22];
}

- (void)dealloc {
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end

RUN>

================打印结果================
2021-04-16 15:55:57.767175+0800 Interview01[5155:149188] 监听到<MJPerson: 0x600002dc42d0>的age属性值改变了 - {
    kind = 1;
    new = 21;
    old = 21;
} - 123

发现监听到了person1属性值得变化,而person2并没有变化,为什么会这样呢?我们看看他们给age赋值的方法:[self.person1 setAge:21];[self.person2 setAge:22];

#import "MJPerson.h"

@implementation MJPerson

- (void)setAge:(int)age
{
    _age = age;
}

//- (int)age
//{
//    return _age;
//}

@end

person1person2setter方法都是一样的,为什么结果会有这么大的差别呢?

首先问题肯定不会在setter方法上,因为两个setter都是一样的,问题就是出在person1person2两个对象上,我们打印person1person2isa看看:

image-20210416160045620

可以发现,person1添加监听后isa是NSKVONotifying_MJPerson,person2不添加监听isa还是MJPerson

NSKVONotifying_MJPerson是系统利用Runtime动态创建的一个类,是MJPerson的子类。

既然NSKVONotifying_MJPerson也是一个类,那么它肯定也有自己的isa和superclass,未使用KVO和使用KVO,实例对象和类对象内存结构如下:

未使用KVO
使用KVO

当person2不添加监听的时候,值改变,会通过person2的isa找到MJPerson,然后再找到MJPerson里面的setAge方法调用,完成。
当person1添加监听的时候,值改变,会通过person1的isa找到NSKVONotifying_MJPerson,然后调用NSKVONotifying_MJPerson的setAge方法(方法内部会调用Foundation框架的_NSSetIntValueAndNotify),不会直接调用MJPerson的setAge方法了。

NSKVONotifying_MJPerson重写父类的setAge:方法内部调用了Foundation框架的_NSSetIntValueAndNotify方法.通过方法名我们可以大概猜测出在这个方法内部先设置了属性的值,然后再通知外部.它的伪代码大概如下:

#import "MJPerson.h"

@interface NSKVONotifying_MJPerson : MJPerson

@end

#import "NSKVONotifying_MJPerson.h"

@implementation NSKVONotifying_MJPerson

- (void)setAge:(int)age
{
    _NSSetIntValueAndNotify();
}

// 伪代码
void _NSSetIntValueAndNotify()
{
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key
{
    // 通知监听器,某某属性值发生了改变
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end

现在我们终于明白了KVO的大概原理,我们在给一个对象添加KVO时,runtime会动态的生成一个相关联的派生类,然后重写了setter方法,在setter方法内部进行了一些的操作,达到监听的目的.

总结:

  1. 使用KVO,系统会使用Runtime动态创建的一个NSKVONotifying_MJPerson类,这个类是MJPerson的子类
  2. 添加监听的属性的值改变的时候,会调用NSKVONotifying_MJPerson类的setAge方法,setAge方法里面会调用_NSSetIntValueAndNotify方法,_NSSetIntValueAndNotify里面走如下步骤:
    ① willChangeValueForKey 将要改变
    ② setAge(原来的set方法) 真的去改变
    ③ didChangeValueForKey 已经改变
    ④ observeValueForKeyPath:ofObject:change:context: 监听到MJPerson的age属性改变了

验证_NSSetIntValueAndNotify

首选我们在person1添加KVO之前和之后分别打印person1person2的类对象看看:

    NSLog(@"person1添加KVO监听之前 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);
    
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    
    NSLog(@"person1添加KVO监听之后 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

RUN>

================打印结果================
2021-04-16 16:12:30.888688+0800 Interview01[5696:209723] person1添加KVO监听之前 - 0x104c604b0 0x104c604b0
2021-04-16 16:12:30.888920+0800 Interview01[5696:209723] person1添加KVO监听之后 - 0x7fff207bc2b7 0x104c604b0

可以发现,添加KVO之前setAge:方法实现都相同,添加KVO之后,person1setAge:方法实现发生改变了. 那我们怎么知道的setAge:方法中调用了_NSSetIntValueAndNotify方法的呢?

我们在LLDB中打印这两个地址的实现

(lldb) p (IMP) 0x7fff207bc2b7
(IMP) $0 = 0x00007fff207bc2b7 (Foundation`_NSSetIntValueAndNotify)
(lldb) p (IMP) 0x104c604b0
(IMP) $1 = 0x0000000104c604b0 (Interview01`-[MJPerson setAge:] at MJPerson.m:13)
(lldb) 
image-20210416161737991

person1添加KVO后,他的setAge:方法实现就是_NSSetIntValueAndNotify.

添加KVO后的person1的类对象和元类对象和person2一样吗?

 NSLog(@"person1添加KVO监听之前 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];   
    
        NSLog(@"类对象 - %@ %@",
          object_getClass(self.person1),  // self.person1.isa
          object_getClass(self.person2)); // self.person2.isa

    NSLog(@"元类对象 - %@ %@",
          object_getClass(object_getClass(self.person1)), // self.person1.isa.isa
          object_getClass(object_getClass(self.person2))); // self.person2.isa.isa

RUN>

================打印结果================
2021-04-16 16:22:35.516106+0800 Interview01[5795:223652] person1添加KVO监听之前 - MJPerson MJPerson
2021-04-16 16:22:35.516327+0800 Interview01[5795:223652] 类对象 - NSKVONotifying_MJPerson MJPerson
2021-04-16 16:22:35.516430+0800 Interview01[5795:223652] 元类对象 - NSKVONotifying_MJPerson MJPerson

通过打印结果可以看出,添加KVO后类对象和元类对象都是runtime动态生成的,跟之前的并不一样.,添加KVO之后,person1的类对象和元类对象都是NSKVONotifying_MJPerson类型

验证_NSSetIntValueAndNotify内部方法调用流程

伪代码演示了_NSSetIntValueAndNotify方法内部大概如下:

- (void)setAge:(int)age
{
    _NSSetIntValueAndNotify();
}

// 伪代码
void _NSSetIntValueAndNotify()
{
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key
{
    // 通知监听器,某某属性值发生了改变
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

验证过程也很简单,重写MJPerson类的三个方法,如下:

#import "MJPerson.h"

@implementation MJPerson

- (void)setAge:(int)age
{
    _age = age;
    
    NSLog(@"setAge:");
}
//- (int)age
//{
//    return _age;
//}

- (void)willChangeValueForKey:(NSString *)key
{
    [super willChangeValueForKey:key];
    
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey - begin");
    
    [super didChangeValueForKey:key];
    
    NSLog(@"didChangeValueForKey - end");
}

@end

RUN>

================打印结果================
2021-04-16 16:30:36.386996+0800 Interview01[5836:233476] willChangeValueForKey
2021-04-16 16:30:36.387173+0800 Interview01[5836:233476] setAge:
2021-04-16 16:30:36.387257+0800 Interview01[5836:233476] didChangeValueForKey - begin
2021-04-16 16:30:36.387593+0800 Interview01[5836:233476] 监听到<MJPerson: 0x6000008405b0>的age属性值改变了 - {
    kind = 1;
    new = 21;
    old = 1;
} - 123
2021-04-16 16:30:36.387677+0800 Interview01[5836:233476] didChangeValueForKey - end

从打印结果可以看出来的确是先调用willChangeValueForKey,然后又调用了[super setAge:age],先进入didChangeValueForKey方法,然后再发出通知

验证重写class、dealloc、isKVO方法

image-20210416163724557

面说过NSKVONotifying_MJPerson类中有两个成员变量isasuperClass和4个方法setAge:,class,dealloc,_isKVO.下面我们写一个方法证明一下NSKVONotifying_MJPerson的确存在这几个方法.setAge方法我们知道为什么重写,但是为什么要重写后面三个方法呢?
首先我们先验证NSKVONotifying_MJPerson的确有这四个方法: 写一个方法,打印输出一个类所有的方法:

//获取一个类里面所有的方法
- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    //c语言中,如果数组是create或者copy出来的要free  OC中ARC不用管
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    
    [self printMethodNamesOfClass:object_getClass(self.person1)];
    [self printMethodNamesOfClass:object_getClass(self.person2)];
}

RUN>

================打印结果================
2021-04-16 16:39:39.741528+0800 Interview01[5920:244465] NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
2021-04-16 16:39:39.741651+0800 Interview01[5920:244465] MJPerson setAge:, age,

由打印结果可知:
NSKVONotifying_MJPerson里面的确有setAge:、class、dealloc、_isKVOA四个方法
MJPerson里面有setAge:、age两个方法

为什么NSKVONtifying_HHPerson类中为什么会重写class方法?如果不重写会怎么样?

我们知道打印一个类的类对象有好几种方法:object_getClassclass.我们分别用这两种方法打印对比一下:

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

RUN>

================打印结果================
2021-04-16 16:43:29.670250+0800 Interview01[5948:249055] person1: NSKVONotifying_MJPerson,  person2:MJPerson
2021-04-16 16:43:29.670334+0800 Interview01[5948:249055] person1: MJPerson, person2:MJPerson

从打印结果可以看到,object_getClass打印出了真实类型,而class打印的结果都是一样,这是因为苹果官方并不想暴露NSKVONotifying_MJPerson这个类,它想隐藏KVO实现的细节,如果没有重写class这个方法,那么最终会找到NSObject中的class方法,执行object_getClass(self)打印的还是NSKVONotifying_HHPerson.所以苹果官方为了隐藏KVO的实现细节,重写了class方法.

其实,因为NSKVONotifying_MJPerson是内部创建的,不想让用户看到,所以用户调用class方法要把NSKVONotifying_MJPerson转成MJPerson,所以系统才重写了class方法。使用object_getClass函数(RuntimeAPI)获取的就是真实的,不会被转成MJPerson。

如果NSKVONotifying_MJPerson没有实现class方法,最后会调用到NSObject的class方法,会直接返回NSKVONotifying_MJPerson,因为NSObject内部这样实现的:

@implementation NSObject
- (Class)class
{
    return object_getClass(self);
}
@end

我们可以写NSKVONotifying_MJPerson的伪代码:

#import "NSKVONotifying_MJPerson.h"

@implementation NSKVONotifying_MJPerson

//NSKVONotifying_MJPerson内部实现了setKey class dealloc isKVO 方法

- (void)setAge:(int)age
{
    _NSSetIntValueAndNotify();
}

// 屏蔽内部实现,隐藏了NSKVONotifying_MJPerson类的存在
- (Class)class
{
    return [MJPerson class];
}

- (void)dealloc
{
    // 收尾工作
}

- (BOOL)_isKVOA
{
    return YES;
}
@end

面试题

下面我们就可以回答面试题了:

问题一:iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
答:

  1. 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
  2. 当修改instance对象的属性时,会先调用这个新子类的setter方法,这个新子类的setter方法内部会调用Foundation的_NSSet*ValueAndNotify函数(内部调用如下方法)
    • willChangeValueForKey:
    • 父类原来的setter
    • didChangeValueForKey:
      内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)

问题二:如何手动触发KVO?(就算没有人修改age值,也想触发监听方法observeValueForKeyPath)
答:手动调用willChangeValueForKey:和didChangeValueForKey:

比如:

[self.person1 willChangeValueForKey:@"age"];
self.person1->_age = 2;
[self.person1 didChangeValueForKey:@"age"];

问题三:直接修改成员变量会触发KVO吗?
答:不会触发KVO,因为没调用重写后的set方法。

比如,如下代码,不会触发

self.person1->_age = 2; 
特别备注

本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

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

推荐阅读更多精彩内容