IOS底层 - KVO原理分析

665 阅读7分钟
从几个面试题出发:
1.KVO的底层是如何实现的?
2.addObserver:forKeyPath:options:context:的context有什么用?
3.直接修改成员变量会触发KVO吗?
4.我们知道KVC会修改成员变量,那么它会触发KVO吗?
5.如何监听可变数组的内容修改?

看到上述问题,你有答案了吗?如果你有疑惑,带着疑问我们开启一段KVO的探索之旅。

1、KVO简介

KVO全称Key-Value Observing,是苹果提供的一套事件通知机制,允许一个对象在其他对象的指定属性发生更改时得到通知的机制。

2、KVO初探

大家都了解KVO的基本使用方法,无非就是添加观察者、接收通知和移除观察者,下面我们通过一个简单的Demo来了解一下具体的实现。

2.1 KVO的简单使用

#import "ViewController.h"

#import "SSBoy.h"

#import "SSGirl.h"

#import <objc/runtime.h>



@interface ViewController ()

@property (nonatomic, strong) SSBoy *boy;
@property (nonatomic, strong) SSGirl *girl;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.boy = [SSBoy new];
    self.girl = [SSGirl new];
    // 添加观察者
    [self.boy addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self.boy addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
    [self.girl addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 修改相应的值
    
    self.boy.name = @"sanliangsan";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    // 接收通知回调
    
    if ([object isEqual:self.boy]) {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"boy change");
        }
    } else {
        NSLog(@"girl change");
    }
}
@end

2.1 问题所在?

上述代码清晰标注了KVO的简单实现,不过这份简单代码有一些问题哦?那么问题在哪呢?

1.boygirl同时观察了相同的name属性,我们的observeValueForKeyPath方法的接收中多层的嵌套判断比较复杂,而且还容易出错,这就引出了上述关于context的面试题,我们引用一段官方文档:

    You may specify NULL and rely entirely on the key path string
to determine the origin of a change notification, but this
approach may cause problems for an object whose superclass is
also observing the same key path for different reasons.
    A safer and more extensible approach is to use the context to
ensure notifications you receive are destined for your
observer and not a superclass

我们可以指定NULL作为context,但是这样会因为一些不同的原因导致对象的父类也同时会观察相同的属性key,使用context可以更安全以及更具有扩展性。同时也告知了我们context如果为空应该是用NULL 而非nil

Context具体如何使用呢

// 定义context

static void *BoyNameContext = &BoyNameContext;
// 添加观察者
[self.boy addObserver:self forKeyPath:@"name"options:NSKeyValueObservingOptionNew context:BoyNameContext];
// 接收通知回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == BoyNameContext) {
        // do....
    }
}

Context你会用了吗?聪明的你可能还发现了最上面的简单例子还有一个致命的问题?

正是:我们没有对相应观察者进行移除,在我们的观察者释放的时候我们要移除相应观察。

- (void)dealloc {
    [self.boy removeObserver:self forKeyPath:@"name" context:BoyNameContext];
}

如果观察的对象是一个单例,而他在几个不同的场景都有观察同样的属性,那么在某个场景消失的时候别的地方触发属性修改就会导致单例去寻找已经释放的对象,就是野指针的情况。具体的实现还请各位自己去测试。到此KVO的简单实现你会了吗?

接下来我们看看KVO底层到底是如何实现的。

3、KVO底层原理

3.1 动态子类

我们依旧来一段文档引出我们今天的核心原理:

    Automatic key-value observing is implemented using a technique 
called isa-swizzling.
    When an observer is registered for an attribute of an object
the isa pointer of the observed object is modified, pointing
to an intermediate class rather than at the true class. As a
result the value of the isa pointer does not necessarily
reflect the actual class of the instance.
    You should never rely on the isa pointer to determine class
membership. Instead, you should use the class method to
determine the class of an object instance.

KVO底层的实现是运用了一项 isa-swizzling 的技术,当我们添加观察者的时候,系统动态的给我们的对象创建了一个子类,将对象的isa指向了动态子类,而KVO的所有实现都是通过这个动态子类的,添加一个动态子类让类的职责更单一具体,而且让我们的KVO透明化,创建动态子类的过程我们是无法感知的,同时我们也知道了获取一个类不能通过isa的指向而是要看class的方法返回。多说无益,我们验证一下:

还是同样的代码,我们看到添加观察者之前,isa指向SSBoy,但是添加观察者之后就指向一个叫NSKVONotifying_SSBoy的类,这正好验证了上述文档所说。

3.1 成员变量和属性

KVO到底研究的是什么?

说到属性,我们无不和实例变量牵扯到一起,他们之间的区别就是是否有setter方法,属性的修改我们在最初已经验证过了,现在我们看看修改实例变量是否会触发KVO

// 实例变量

@interface SSBoy : NSObject
{
    @public
    
    NSString *nickName;
}
 
 // 添加观察者
[self.boy addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:BoyNameContext];

// 修改实例变量
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 修改相应的值
    
    self.boy->nickName = @"sanliangsan";
}
// 没有响应
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    // 接收通知回调
    
    if (context == BoyNameContext) {
    }
}

最终我们的结果是不会触发,那么为什么成员变量的修改不会触发KVO,动态子类究竟都干了啥?

3.3 动态子类做了啥?

了解一个类我们无非是从属性,成员变量,方法等去研究,这里我们从方法入手,其他的大家可以下去一一验证。

我们去打印原类和动态子类的所有方法作以对比。

1.NSKVONotifying_SSBoy中为啥没有setAge?

因为没有针对age添加观察者,这也证明了KVO的动态中间子类是通过实现setter方法去实现的。

2.NSKVONotifying_SSBoy中为啥有class方法?

重写class方法,因为class指向类本身,【伪装】为了让这一层更透明,苹
果重写class方法重新指向SSBoy,让上层对动态子类的生成没有感知,透明化&隐私化

3.dealloc方法为了啥?

最初我们已经了解了,在添加观察者的时候会动态生成子类,而且对
象的isa会指向动态子类,当动态子类调用dealloc的时候,isa当然会重新指向回原类。

3.4 KVO与KVC

之前的一篇文章 KVC原理与自定义 有讲述KVC底层是如何一步一步实现修改对象的属性的,那么问题来了,KVC会触发KVO吗,仔细阅读KVO官方文档我们看到一段话:

NSObject provides a basic implementation of automatic ke
y-value change notification. Automatic key-value change no
tification informs observers of changes made using key-val
ue compliant accessors, as well as the key-value coding me
thods. Automatic notification is also supported by the col
lection proxy objects returned by, for example, mutableAr
rayValueForKey:

大概意思就是NSObject提供了自动key-value观察的实现,而且通过setter方法和key-value coding方法是一样的,换言之:key-value coding也能实现自动KVO,同时文档还给出相关能触发KVO的实例:

Examples of method calls that cause KVO change notifications to be emitted

// Call the accessor method.
[account setName:@"Savings"];
 // Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
 // Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

通过以上我们得知:KVC是能自动实现KVO的,而且可以验证不论是否有某属性都会自动通知到观察者。

3.5 KVO与可变数组

我们在某些情况下想对一个数组进行观察,添加、删除,修改等等,但是实际测试发现,普通的方法调用并不会触发KVO,其原因很简单,利用我们上述的原理就得以解释:我们对数组的各种添加、删除、修改并不会调用setter方法,由于KVC会触发KVO我们在KVC里边找到相关的方法得以实现:

[[_arrayModel mutableArrayValueForKeyPath:@"dataArray"] addObject:XXX];
[[_arrayModel mutableArrayValueForKeyPath:@"dataArray"] removeObject:XXX];

4.总结

至此,我们对KVO的简单使用以及原理分析已经完结,那些面试题的答案你都知晓了吗?实际的使用过程中我们还会碰到更多的问题,期待你的交流和沟通。