iOS-底层-KVO和KVC

一. KVO

1. KVO的基本使用

KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变

KVO.png

添加监听:

// 给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;
}

监听改变:

//context:@"123" 作用:在添加监听的时候传入,传到下面这个方法里面
/**
 当监听对象的属性值发生改变时,就会调用
 @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);
}

移除监听:

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

2. 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

打断点,分别po它们的isa

(lldb) po self.person1.isa
NSKVONotifying_MJPerson

  Fix-it applied, fixed expression was: 
    self.person1->isa
(lldb) po self.person2.isa
MJPerson

  Fix-it applied, fixed expression was: 
    self.person2->isa
(lldb) 

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

  1. 其实NSKVONotifying_MJPerson是系统利用Runtime动态创建的一个类,是MJPerson的子类。
  2. 如果你自己写了这个类,就会报动态生成失败。
  3. KVO效率没代理高,因为代理是直接调用,KVO还要动态生成一个类。

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

未使用KVO.png
使用KVO.png

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

由于无法查看Foundation框架的实现,我们写一些伪代码,来表明添加监听后的方法调用顺序,创建NSKVONotifying_MJPerson类继承于MJPerson。

#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

总结:

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

3. 验证_NSSetIntValueAndNotify内部方法调用流程

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

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

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

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

赋值之后运行,打印结果如下:

willChangeValueForKey
setAge:
didChangeValueForKey - begin
监听到<MJPerson: 0x6000017c90f0>的age属性值改变了 - {
    kind = 1;
    new = 21;
    old = 1;
} - 123
didChangeValueForKey - end

4. 验证生成了NSKVONotifying_MJPerson

结论我们知道了,接下来还是用代码验证一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    NSLog(@"person1添加KVO监听之前 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));
    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监听之后 - %@ %@",
          object_getClass(self.person1),
          object_getClass(self.person2));
    NSLog(@"person1添加KVO监听之后 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);
}

如上,我们在添加KVO之前和之后分别打印它的类对象和它的setAge方法实现,结果如下:

person1添加KVO监听之前 - MJPerson MJPerson
person1添加KVO监听之前 - 0x10ede8590 0x10ede8590
person1添加KVO监听之后 - NSKVONotifying_MJPerson MJPerson
person1添加KVO监听之后 - 0x10f143216 0x10ede8590

可以发现:
person1添加KVO之前,person1和person2的类对象和setAge方法都是一样的。
person1添加KVO之后,person1的isa指向的类对象变成了NSKVONotifying_MJPerson,setAge方法地址也变了,person2什么都没变。

接下来,我们通过p (IMP)指令打印某个地址对应的实现:

(lldb) p (IMP)0x10ede8590
(IMP) $0 = 0x000000010ede8590 (Interview01`-[MJPerson setAge:] at MJPerson.m:13)
(lldb) p (IMP)0x10f143216
(IMP) $1 = 0x000000010f143216 (Foundation`_NSSetIntValueAndNotify)
(lldb) 

打印发现,添加KVO之后果然调用的是Foundation框架下的_NSSetIntValueAndNotify函数,说明我们上面的结论是正确的。

5. NSKVONotifying_MJPerson类对象的isa指向哪里?

下面还有最后一个问题,NSKVONotifying_MJPerson类对象的isa指向哪里呢?这个验证起来也很简单,用如下代码打印:

//获取类对象
NSLog(@"类对象 - %p %p",
      object_getClass(self.person1),  // 相当于获取person1的类对象(self.person1.isa)
      object_getClass(self.person2)); // 相当于获取erson2的类对象(self.person2.isa)

//获取元类对象
NSLog(@"元类对象 - %p %p",
      object_getClass(object_getClass(self.person1)), // 相当于获取person1类对象的元类对象(self.person1.isa.isa)
      object_getClass(object_getClass(self.person2))); // 相当于获取相当于获取person1类对象的元类对象(self.person2.isa.isa)

打印结果如下;

类对象 - 0x6000008f0090 0x100d91158
元类对象 - 0x6000008f1050 0x100d91180

可以发现person1和person2的类对象和元类对象的地址都不一样,说明他们是不同的类。这和我们以前的结论一致,所以NSKVONotifying_MJPerson类对象的isa也是指向它自己的元类对象

注意:当属性是int类型的时候调用的是_NSSetIntValueAndNotify方法,当属性是float类型的时候调用的是_NSSetFloatValueAndNotify方法,其他类比_NSSetObjectValueAndNotify,_NSSetLongValueAndNotify等等......

6. 为什么重写class、dealloc、isKVO方法

在第三张图中我们可以看出,创建NSKVONotifying_MJPerson之后会重写setAge、class、dealloc、isKVO 这四个方法,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)];
}

打印结果如下:

打印结果:
NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
MJPerson setAge:, age,

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

接下来如果想知道为什么要重写class、dealloc、_isKVOA三个方法,我们先打印:

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

打印结果:

NSKVONotifying_MJPerson MJPerson
MJPerson MJPerson

OC对象的分类中,我们知道上面两种方式都可以获取类对象,但是为什么获取的结果不一样呢?

其实,因为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

7. 面试题

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

问题一: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"];
//didChangeValueForKey内部会判断willChangeValueForKey是否调用,所以两个都要调用

会打印:

2019-04-11 15:34:31.968473+0800 Interview01[16217:3596188] 监听到<MJPerson: 0x6000000180d0>的age属性值改变了 - {
    kind = 1;
    new = 2;
    old = 1;
} - 123

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

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

self.person1->_age = 2; 

二. KVC

1. KVC的基本使用

KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性

常见的API有:

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;

KVC的基本使用:

person.age = 10;

NSLog(@"%@", [person valueForKey:@"age"]);
NSLog(@"%@", [person valueForKeyPath:@"cat.weight"]);
NSLog(@"%d", person.age);

//[person setValue:[NSNumber numberWithInt:10] forKey:@"age"];
[person setValue:@10 forKey:@"age"];

person.cat = [[MJCat alloc] init];
[person setValue:@10 forKeyPath:@"cat.weight"];
//setValue:@10 forKeyPath 更强大推荐使用.
NSLog(@"%d", person.age);

2. 通过KVC给属性赋值能触发KVO吗?

能不能试一下就知道了:

MJObserver *observer = [[MJObserver alloc] init];
MJPerson *person = [[MJPerson alloc] init];
// 添加KVO监听
[person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
[person setValue:@10 forKey:@"age"];

打印如下:

2019-04-11 15:55:00.568868+0800 Interview01-KVC[16345:3639722] observeValueForKeyPath - {
    kind = 1;
    new = 10;
    old = 0;
}

发现,通过KVC修改age属性会触发KVO。为什么呢?先往下看

接下来看看setValue:forKey:设值原理和valueForKey:取值原理。

3. KVC设值原理

设值原理.png

设值原理解释:

1.寻找setAge方法
- (void)setAge:(int)age
{
    NSLog(@"setAge: - %d", age);
}

2.寻找_setAge方法
- (void)_setAge:(int)age
{
    NSLog(@"_setAge: - %d", age);
}

3.找不到上面两个方法就调用accessInstanceVariablesDirectly问问能不能直接访问成员变量
 默认的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
    return YES;
}

4.1 如果返回NO,就调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException
4.2如果返回YES,会按顺序_key,_isKey,key,isKey赋值,如果四个都找不到就报上面的错

上面我们知道KVC设值会触发KVO,但是如果没有set方法呢?通过验证(验证过程省略)可知,就算没有set方法只有成员变量,通过KVC进行赋值也会触发KVO,可以理解它们是配套使用的。

其实KVC内部调用了下面方法才会触发KVO的,可自行验证。

[person willChangeValueForKey:@"age"];
person->_age = 10;
[person didChangeValueForKey:@"age"];

4. KVC取值原理

取值原理.png

KVC取值原理解释,可自行验证:

1.getAge
- (int)getAge
{
    return 11;
}

2.age
- (int)age
{
    return 12;
}

3.isAge
- (int)isAge
{
    return 13;
}

4._age
- (int)_age
{
    return 14;
}

5.3.找不到上面四个方法就调用accessInstanceVariablesDirectly问问能不能直接访问成员变量
 默认的返回值就是YES
+ (BOOL)accessInstanceVariablesDirectly
{
    return YES;
}

5.1 如果返回NO,就调用valueforUndefinedKey:并抛出异常NSUnknownKeyException
5.2如果返回YES,会按顺序_key,_isKey,key,isKey赋值,如果四个都找不到就报上面的错

Demo地址:KVO和KVC原理

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