底层原理:KVO

2022-01-11  本文已影响0人  飘摇的水草
面试题
举例

我们用一个代码例子来总结原理,首先创建一个 MyPerson 类,里面只有一个 age 的属性:

@interface MyPerson : NSObject
@property (assign, nonatomic) int age;
@end

然后在外部控制器中我们用 MyPerson 声明两个示例对象 person1person2。只对 person1 添加 KOV 监听,然后通过打印监听前和监听后的内存地址,来看他们的 isa 指针指向是否有变化,并且打印 person1 和 person2 的类对象和元类对象进行对比,methodForSelector用于打印出方法实现。

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MyPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MyPerson 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:)]);

    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
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // NSKVONotifying_MyPerson是使用Runtime动态创建的一个类,是MyPerson的子类
    // self.person1.isa == NSKVONotifying_MyPerson
    //[self.person1 setAge:21];
    self.person1.age = 20;
    self.person2.age = 20;
}

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

打印结果:

person1添加KVO监听之前 - MyPerson MyPerson
person1添加KVO监听之前 - 0x1078b6440 0x1078b6440
person1添加KVO监听之后 - NSKVONotifying_MyPerson MyPerson
person1添加KVO监听之后 - 0x7fff207bf79f 0x1078b6440
类对象 - NSKVONotifying_MyPerson MyPerson
元类对象 - NSKVONotifying_MyPerson MyPerson

可以看出,在添加监听之前和之后methodForSelector打印出的内存地址发生了变化,可以在控制台里打印出这个内存对应的方法:

(lldb) p (IMP)0x1078b6440
(IMP) $0 = 0x000000000456789c0 (Interview01 ` -[MJPerson setAge:]) at MJPerson.m:13

KVO本质分析

我们发现添加KVO监听之前,person1 和 person2 的类对象是一样的都是 MyPerson,但是添加 KVO 监听之后,person1 的类对象变为了NSKVONotifying_MyPerson。

未使用KVO监听的对象:

使用了 KVO 监听的对象:

_setAge 其实是调用了_NSSetIntValueAndNotify 方法,
_NSSetIntValueAndNotify 方法内部其实是如下伪代码:

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

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

Foundation 框架内除了有 _NSSetIntValueAndNotify,还有_NSSetBoolValueAndNotify_NSSetCharValueAndNotify_NSSetDoubleValueAndNotify 等等类型。

_NSSetxxxValueAndNotify 的内部实现

调用顺序如下:

NSKVONotifying_MyPerson 除了重写了 setAge: 方法,还重写了 class(屏蔽内部实现,不让开发者知道)、dealloc、_isKVOA 方法。如果调用下面的代码:

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

会发现打印为:

NSKVONotifying_MyPerson  MyPerson
MyPerson  MyPerson
总结
  1. iOS用什么方法实现对一个对象的kvo,kvo的本质是什么?

    • 利用Runtime API动态生成一个子类,并且让instance对象的isa指向这个全新的子类
    • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
    • willChangeValueForKey:
    • 父类原来的Setter
    • didChangeValueForKey:
    • 内部会触发监听器(Observer)的监听方法(observeValueForKeyPath:ofObject:change:context)
  2. 如何手动触发KVO?

    • 手动调用willChangeValueForKey:和didChangeValueForKey:
  3. 直接修改成员变量会触发KVO吗?

    • didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context方法。
@implementation NSKVONotifying_MJPerson

// 在`setAge:`方法中调用了`_NSSetIntValueAndNotify()`这个C语言函数
- (void)setAge:(int)age
{
   _NSSetIntValueAndNotify()
}

//伪代码
void  _NSSetIntValueAndNotify()
{
   [self willChangeValueForKey:@"age"];
   // 2.调用父类的`setAge:`函数
   [super setAge:age];
   [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key
{
   // 调用监听器的监听方法,通知监听器某一个对象的属性值发生了改变
   [observer observeValueForKeyPath:key ofObject:self change:nil  context:nil];
}
@end
上一篇 下一篇

猜你喜欢

热点阅读