IOS底层 - KVO原理分析

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

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

一、KVO简介

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

二、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的面试题,咱们引用一段官方文档:app

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具体如何使用呢ide

// 定义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你会用了吗?聪明的你可能还发现了最上面的简单例子还有一个致命的问题?post

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

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

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

接下来咱们看看KVO底层究竟是如何实现的。

三、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的简单使用以及原理分析已经完结,那些面试题的答案你都知晓了吗?实际的使用过程当中咱们还会碰到更多的问题,期待你的交流和沟通。

相关文章
相关标签/搜索