程序员社区

IOS-KVO实现的原理和本质

面试题

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

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

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

KVO的基本使用

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

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

IOS-KVO实现的原理和本质插图
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看看:

IOS-KVO实现的原理和本质插图1
image-20210416160045620

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

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

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

IOS-KVO实现的原理和本质插图2
未使用KVO
IOS-KVO实现的原理和本质插图3
使用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) 
IOS-KVO实现的原理和本质插图4
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方法

IOS-KVO实现的原理和本质插图5
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对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

赞(0) 打赏
未经允许不得转载:IDEA激活码 » IOS-KVO实现的原理和本质

一个分享Java & Python知识的社区