03 iOS底层原理 - KVO本质探究

2020-01-29  本文已影响0人  程序小胖

废话不多说先来几个面试题:

一,iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
二,如何手动触发KVO
三,直接修改成员变量会触发KVO吗?

通过挖掘KVO的本质,就会发现,这几个面试题就跟切菜一样

那么什么是KVO呢?

什么是KVO呢?
KVO的全称是Key-Value-Observing,即“键值监听”,可以用于监听某个对象属性值的变化

用一张简单的图就可以表示:

image.png

这就是KVO最简单的是使用,那么它到底是怎么实现的呢,下面我们一步步解开这个KVO底层的神秘面纱。

一,代码准备

1. 创建一个Person 类 继承NSObject
// 声明
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end

// 实现
@implementation Person
- (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");
}
@end
2. ViewController代码
// 导入头文件
#import <objc/runtime.h>
#import "Person.h"

// 声明person对象
@interface ViewController ()
@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    person1 = [[Person alloc]init];
    person1 = 18;

    self.person2 = [[Person alloc]init];
    self.person2.age = 28;
    // 给person1添加一个KVO
    [person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"123"];
}

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person1.age = 20;
}

二,开始研究KVO本质

点击屏幕后,会触发observeValueForKeyPath这监听事件
// 打印结果

监听到<Person: 0x600002e2c510>的age属性值改变了 - {
         kind = 1;
         new = 20;
         old = 18;
 }

发现age的值确实发生了变化

1. 那么age值的改变是否与setAge:方法有关

在touchesBegan给person2重新赋值

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    self.person1.age = 21;
//    self.person2.age = 22;
//  上面两句代码,其实就是调用Person的 set 方法,都在调用同一个方法
    [self.person1 setAge:21];
    [self.person2 setAge:22];
}

当触发touchesBegan事件时,发现调用 [self.person1 setAge:21];后会触发observe监听器,而调用 [self.person2 setAge:22];后不会触发observe监听器。
既然都是在调用 setAge方法,但是为啥只有person1的age发生改变时,才会给observe发通知呢?而person2的age发生改变时,不会通知observe?
先看两张图:

image.png image.png
从上面两张图,可以看出来:     
其实本质上就是两个实例对象的isa指向不一样
现在看下两个实例的isa有什么不同
lldb结果:
self.person1.isa -> NSKVONotifying_Person
self.person2.isa -> Person
差异:
1. 添加了KVO监听的实例person1,通过isa指向的class是NSKVONotifying_Person
2. 没有添加KVO监听的实例person2,通过isa指向的class是Person
那么,NSKVONotifying_Person这个类是怎么产生的呢?
本质上是利用Runtime动态创建的一个类(可以利用truntime机制自己实现KVO监听)

结论:由此可见,age值的改变与setAge:方法没有关系,而是因为派生出了新的类,此时的setAge:方法的实现也就不一样了,具体有啥不易样的,接着往下看哈。

2. person1添加KVO前后person1和person2对象的变化

验证person1在添加监听前后,person1实例的isa指向的【class】和具体的【对象方法】实现到底发生了哪些变化

  1. 利用runtime的 object_getClass() 查看person1添加监听前后的,person1和person2的isa指向类对象;
  2. 利用runtime的 object_getClass() 查看person1添加监听前后的,person1和person2的isa指向类对象的地址;
  3. 利用runtime的 methodForSelector这个方法来获取setAge:这个实例方法的具体实现
    根据以上3点,添加一些打印信息:
    NSLog(@"person1添加监听之前类对象:%@, %@",
          object_getClass(self.person1),
          object_getClass(self.person2));
    NSLog(@"person1添加监听之前类对象地址:%p, %p",
          object_getClass(self.person1),
          object_getClass(self.person2));
    NSLog(@"person1添加监听之前类的实例方法实现:%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添加监听之后类对象:%@, %@",
          object_getClass(self.person1),
          object_getClass(self.person2));
    NSLog(@"person1添加监听之后类对象地址:%p, %p",
          object_getClass(self.person1),
          object_getClass(self.person2));
    NSLog(@"person1添加监听之后类的实例方法实现:%p, %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

分析打印信息:
打印结果:

person1添加监听之前类对象:Person, Person
person1添加监听之后类对象:NSKVONotifying_Person, Person

person1添加监听之前类对象地址:0x1089bd708, 0x1089bd708
person1添加监听之后类对象地址:0x6000021d87e0, 0x1089bd708

结论:
由此可见,person1在添加监听后,isa所指向的类确实改变了,那么setAge:这个对象方法到底是怎么实现的呢,继续分析第三个打印结果

打印结果:

person1添加监听之前类的实例方法实现:0x1061e1ed0, 0x1061e1ed0
person1添加监听之后类的实例方法实现:0x7fff257223da, 0x1061e1ed0

利用lldb查看
(lldb) p (IMP)0x1061e1ed0
(IMP) $0 = 0x00000001061e1ed0 (06-KVO初探`-[Person setAge:] at Person.m:13)
(lldb) p (IMP)0x7fff257223da
(IMP) $1 = 0x00007fff257223da (Foundation`_NSSetIntValueAndNotify)

结论:
由此可见,person1添加监听后,setAge:的实现的确改变了,是在Foundation框架下的_NSSetIntValueAndNotify(c语言方法)实现的,_NSSetIntValueAndNotify具体实现见自己实现的NSKVONotifying_Person 类。

下面可以看下_NSSetIntValueAndNotify实现的伪代码

// 声明一个添加了KVO的派生类 NSKVONotifying_Person
@interface NSKVONotifying_Person : Person
@end

// 实现
@implementation NSKVONotifying_Person
- (void)setAge:(int)age {
    _NSSetIntValueAndNotify();
}
// 伪代码 大概流程
void _NSSetIntValueAndNotify() {
    [self willChangeValueForKey:@"age"];
    [super setAge:age]; 
    [self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
    // 伪代码
    // 通知监听器,某某属性发生了改变
    [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
3. 查看person1添加KVO后,都有哪些调用

在Person类里,实现两个父类的方法,并且给setAge加上打印信息

- (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");
}

触发touchesBegan方法

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.person1 setAge:21];
//    [self.person2 setAge:22];
}

打印结果是:

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

打印结果分析:添加了KVO的对象,属性改变的话,都有下面一些方法调用

当修改instance对象的属性时,先调用setter方法,然后实现Foundation中的_NSSet****ValueAndotify函数
a> willChangeValueForKey:
b> 父类原来的setter
c> didChangeValueForKey:
内部会触发监听器(Observe)的监听方法(observeValueForKeyPath:ofObject:change:context:)

如果person2调用了setAge:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // [self.person1 setAge:21];
    [self.person2 setAge:22];
}

打印结果只有一个

哥们 == setAge

说明:没有添加监听的对象,willChangeValueForKey和didChangeValueForKey方法不会调用

4. 查看NSKVONotifying_Person类的元类

验证一下,添加KVO后,新生成的NSKVONotifying_Person类对象的isa指向的元类对象及对象地址是什么

利用runtime的 Class object_getClass(id obj) 来查看对象地址和对象名
1> 传入的obj可能是instance对象、class对象、meta-class对象
2> 返回值
a) 如果是instance对象,返回class对象
b) 如果是class对象,返回meta-class对象
c) 如果是meta-class对象,返回NSObject(基类)的meta-class对象

为了验证在添加KVO后添加一些打印信息

NSLog(@"类对象地址:%p, %p",
// 拿到person1.isa,相当于Person类地址(isa & ISA_MASK)
          object_getClass(self.person1), 
// 拿到person2.isa
          object_getClass(self.person2)); 

NSLog(@"元类对象地址:%p, %p",
// 拿到person1.isa.isa
          object_getClass(object_getClass(self.person1)), 
// 拿到person2.isa.isa
          object_getClass(object_getClass(self.person2))); 
    
NSLog(@"类对象:%@, %@",
// 拿到person1的isa指向的类
          object_getClass(self.person1), 
// 拿到person2的isa指向的类
          object_getClass(self.person2)); 

NSLog(@"元类对象:%@, %@",
 // 拿到person1的isa指向的类的元类
          object_getClass(object_getClass(self.person1)),
 // 拿到person2的isa指向的类的元类
          object_getClass(object_getClass(self.person2)));

打印信息分析:

类对象地址:0x600001284120, 0x1081ea710
元类对象地址:0x6000012841b0, 0x1081ea6e8

类对象:NSKVONotifying_Person, Person
元类对象:NSKVONotifying_Person, Person

结论:
从打印结果来看,派生出来的NSKVONotifying_Person类的元类就是它本身

三, 派生出来的NSKVONotifying_Person都有哪些方法

先看一张示例图,红框狂起来的部分

image.png
1. 利用runtime来获取方法实现
Class cls = object_getClass(self.person1);
    unsigned int count;
    Method *methodList = class_copyMethodList(cls, &count);
    // 遍历方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"methodName == %@", methodName);
    }

打印结果:

setAge:
class
dealloc
_isKVOA

所以派生出来的NSKVONotifying_Person类里面,确实存在setAge:/class/dealloc/_isKVOA这些方法

2. 研究下为啥要重写class方法呢?

添加两个打印信息

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

上面打印信息是利用两种方法来获取person1和person2的类对象
打印结果:

类对象:NSKVONotifying_Person, Person
类对象:Person, Person

结果分析:
只有通过runtime的object_getClass这中方式,取出来的才是person1的isa指向的类对象,
而通过self.person1 class]取出来的,不一定是person1的isa指向的类对象

为什么要重写class呢?
猜测:系统为了直接返回当前类,屏蔽内部实现,

如果不重写,person1找到isa,通过isa找到对应的类对象,在这个类里面发现没有-class的这个方法,那么就通过superclass找到父类,
如果父类还没有,就通过superclass一直找到基类,基类(NSObject)里面就会通过object_getClass(sef)返回当前的类,即Person

四,回答面试题

一,iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
 1. 利用runtimeAPI动态生成一个子类,并且让instance的isa指向一个全新的子类
 2. 当修改instance对象的属性时,现调用setter方法,然后实现Foundation中的_NSSet****ValueAndotify函数
 a> willChangeValueForKey:
 b> 父类原来的setter
 c> didChangeValueForKey:
    内部会触发监听器(Observe)的监听方法(observeValueForKeyPath:ofObject:change:context:)
 
 二,如何手动触发KVO
 意思就是不通过改变属性的值,怎么触发KVO
 手动调用willChangeValueForKey:和didChangeValueForKey:
 
 三,直接修改成员变量会触发KVO吗?
 不会触发KVO,因为只有通过setter方法才能触发,而成员变量不会调用setter方法
 要想通过修改成员变量来触发KVO,也很简单
 手动依次调用:
 1.willChangeValueForKey:
 2.修改成员变量
 3.didChangeValueForKey:
上一篇下一篇

猜你喜欢

热点阅读