KVO 原理详解

栏目: IOS · 发布时间: 5年前

内容简介:KVO(Key-Value Observing)是 iOS 开发中常用的一种用于监听某个对象属性值变化的技术。下文将以一段示例代码来分析 KVO 的底层原理。示例对通过上述代码可以发现,一旦

KVO(Key-Value Observing)是 iOS 开发中常用的一种用于监听某个对象属性值变化的技术。下文将以一段示例代码来分析 KVO 的底层原理。 源码地址

示例源码

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupSubviews];

    BAOPerson *p1 = [[BAOPerson alloc] init];
    BAOPerson *p2 = [[BAOPerson alloc] init];
    p1.age = 1;
    p1.age = 2;
    p2.age = 2;

    // self 监听 p1 的 age 属性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    p1.age = 10;
    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void)setupSubviews {
    [self setupHeaderView];
}

- (void)setupHeaderView {
    self.headerView.title = @"KVO";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"监听到 %@ 的 %@ 改变了 %@", object, keyPath,change);
}

示例对 p1 进行了 KVO 监听,当 p1 发生改变,即调用 observeValueForKeyPath 方法,从而打印以下信息。

监听到 <BAOPerson: 0x600003750200> 的 age 改变了 {
    kind = 1;
    new = 10;
    old = 2;
}

KVO 实现原理

通过上述代码可以发现,一旦 age 属性的值发生改变,就会通知到监听者。我们知道赋值操作都是调用 set 方法,我们可以重写 BAOPerson 类中 ageset 方法,观察 KVO 是否是在 set 方法内部做了一些操作来通知监听者。

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

我们发现即使重写了 set 方法, p1 除了调用 set 方法之外还会执行监听者的 observeValueForKeyPath 方法。

根据上述实验推测:KVO 在运行时对 p1 对象进行了改动,使 p1 对象在调用 setAge 方法时做了一些额外的操作。所以问题出在对象身上,两个对象可能本质上并不一样。下面我们来探索一下 KVO 内部是如何实现的。

KVO 实现分析

首先分别在添加 KVO 前后打上断点,以观察添加 KVO 前后 p1 对象有何不同。

KVO 原理详解

通过打印对象的 isa 指针,我们发现, p1 对象的 isa 指针由之前的指向类对象 BAOPerson 变成了指向类对象 NSKVONotifying_BAOPerson 。相应地, p2 对象没有改变。因此我们可以推测, p1 对象的 isa 发生改变后,其执行的 setAge 也发生了改变。

我们知道, p2 在调用 setAge 方法时,首先会通过 p2 对象的 isa 指针找到 BAOPerson 类对象,然后在类对象中找到 setAge 方法,最终找到方法对应的实现。如下图所示:

KVO 原理详解

但是, p1 对象的 isa 在添加 KVO 之后已经指向了 NSKVONotifying_BAOPerson 类对象, NSKVONotifying_BAOPerson 则是 BAOPerson 的子类。 NSKVONotifying_BAOPerson 是 runtime 在运行时生成的。因此, p1 对象在调用 setAge 方法时必然会根据 p1isa 找到 NSKVONotifying_BAOPerson ,并在 NSKVONotifying_BAOPerson 中找到 setAge 方法及其实现。

经查阅资料了解到, NSKVONotifying_BAOPerson 中的 setAge 方法中其实调用了 Foundation 框架中 C 语言函数 _NSsetIntValueAndNotify_NSsetIntValueAndNotify 内部的操作大致是:首先调用 willChangeValueForKey 方法,然后调用父类的 setAge 方法对成员变量赋值,最后调用 didChangeValueForKey 方法。 didChangeValueForKey 方法中会调用监听者的监听方法,最终调用监听者的 observeValueForKeyPath 方法。

KVO 原理验证

前面我们已经通过断点打印 isa 指针的方式验证了: p1 对象在添加 KVO 后,其 isa 指针会指向一个通过 runtime 创建的 BAOPerson 的子类 NSKVONotifying_BAOPerson

下面我们可以通过打印方法实现的地址来看一下 p1p2setAge 方法实现的地址在添加 KVO 前后有什么变化。

// 通过methodForSelector找到方法实现的地址
NSLog(@"添加 KVO 之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)], [p2 methodForSelector: @selector(setAge:)]);
    
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"添加 KVO 之后 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)], [p2 methodForSelector: @selector(setAge:)]);

KVO 原理详解

执行上述代码,可以发现:在添加 KVO 之前, p1p2setAge 方法实现的地址是相同的;在添加 KVO 之后, p1setAge 方法实现的地址发生了改变。通过打印方法实现可以证明, p1setAge 方法的实现由 BAOPerson 类方法中的 setAge 方法转换成了 Foundation 框架中的 C 函数 _NSSetIntValueAndNotify

事实上,Foundation 框架中很多例如 _NSSetBoolValueAndNotify_NSSetCharValueAndNotify_NSSetFloatValueAndNotify_NSSetLongValueAndNotify 等函数。

为了查看 Foundation 框架中的相关函数,我们找到 Foundation 文件,通过命令行查询:

nm Foundation | grep ValueAndNotify

KVO 原理详解

中间类内部结构

NSKVONotifying_BAOPerson 作为 BAOPerson 的子类,其 superclass 指针指向 BAOPerson 类,其内部对 setAge 方法做了单独的实现,那么 NSKVONotifying_BAOPersonBAOPerson 类的差别可能就在于其内存储的对象方法及实现不同。我们通过 runtime 分别打印 BAOPerson 类对象和 NSKVONotifying_BAOPerson 类对象内存储的对象方法。

- (void)viewDidLoad {
    [super viewDidLoad];

    BAOPerson *p1 = [[BAOPerson alloc] init];
    BAOPerson *p2 = [[BAOPerson alloc] init];
    p1.age = 1;
    p1.age = 2;
    p2.age = 2;
    
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];

    [self printMethods: object_getClass(p2)];
    [self printMethods: object_getClass(p1)];
    
    p1.age = 10;
    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void)printMethods:(Class)cls {
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@ - ", cls];

    for (int i = 0 ; i < count; i++) {
        Method method = methods[i];
        NSString *methodName  = NSStringFromSelector(method_getName(method));

        [methodNames appendString:methodName];
        [methodNames appendString:@" "];

    }

    NSLog(@"%@", methodNames);
    free(methods);
}

上述代码的打印结果如下:

KVO 原理详解

可以发现, NSKVONotifying_BAOPerson 中有 4 个对象方法,分别是:

setAge:
class
dealloc
_isKVOA

NSKVONotifying_BAOPerson 重写 class 方法是为了隐藏 NSKVONotifying_BAOPerson 不被外界看到。我们在 p1 添加 KVO 之后,分别打印 p1p2 对象的 class ,可以发现它们都返回 BAOPerson

NSLog(@"%@, %@", [p1 class], [p2 class]);
// 打印结果 BAOPerson, BAOPerson

综上,我们可以画出 NSKVONotifying_BAOPerson 的内部结构及方法调用顺序。

KVO 原理详解

验证 didChangeValueForKey: 内部调用 observeValueForKeyPath:ofObject:change:context: 方法

BAOPerson 类中重写 willChangeValueForKey:didChangeValueForKey: 方法,模拟它们的实现。

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

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

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

通过运行上述代码,可以确定是在 didChangeValueForKey: 方法内部调用了监听者的 observeValueForKeyPath:ofObject:change:context: 方法。

KVO 原理详解

根据上述原理,可以通过调用 willChangeValueForKey:didChangeValueForKey: 来手动触发 KVO。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

用户至上:用户研究方法与实践(原书第2版)

用户至上:用户研究方法与实践(原书第2版)

凯茜·巴克斯特 / 王兰、杨雪、苏寅 / 机械工业出版社 / 2017-5-1 / 99

《UI/UE系列丛书 用户至上:用户研究方法与实践(原书第2版)》是用户研究方法指南,谷歌用户体验研究员十几年工作经验结晶,从理论到实战,包含完整的实操案例,是设计以人为中心产品的实用手册。 《UI/UE系列丛书 用户至上:用户研究方法与实践(原书第2版)》包含五个部分共15章。入门篇包括第1~5章:介绍用户体验入门,如何理解目标用户,道德与法律问题,如何搭建研究设施,如何选择用户体验研究方......一起来看看 《用户至上:用户研究方法与实践(原书第2版)》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具