浅析 KVO 内部实现

KVO 全称是Key Value Observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。

KVO 的基本使用:

(1)注册指定Key路径的监听器:

 /** 参数
         *  addObserver: 监听对象
         *  forKeyPath: 监听属性Key
         *  options: 监听可选项
         *      NSKeyValueObservingOptionNew: 监听改变后的新值
         *      NSKeyValueObservingOptionOld: 监听改变后的旧值
         *  context: 传入的上下文
         */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

(2)删除指定Key路径的监听器:

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

(3)回调监听:

- (void)observeValueForKeyPath:(NSString *)keyPath //监听的属性值
                      ofObject:(id)object          //监听的对象
                        change:(NSDictionary<NSString *,id> *)change //值的改变(由options参数决定传入新值或者旧值)
                       context:(void *)context //传入的上下文内容

值得注意的是:不要忘记解除注册,否则会导致资源泄露。

设置属性

将观察者与被观察者注册好之后,就可以对观察者对象的属性进行操作,这些变更操作就会被通知给观察者对象。注意,只有遵循 KVO 方式来设置属性,观察者对象才会获取通知,也就是说遵循使用属性的 setter 方法,或通过 key-path 来设置:

target.age = 30;
[target setAge:30]; 
[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];

下面看一个小 Demo:
我们设想一个场景,当自己住酒店的时候,当酒店给我换房间的时候,我们要得到提醒,才能找对自己的房间。我们依次为例:

//ViewController
#import "ViewController.h"
#import "Person.h"
#import "Room.h"

@interface ViewController ()
@property (nonatomic, strong) Person *person;
@property (nonatomic, strong) Room *room;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[Person alloc] init];
    self.room = [[Room alloc] init];
    //设置房间的号码
    self.room.no = 10;

    //Person 监听 Room 编号的变化
    [self.room addObserver:self.person forKeyPath:@"no" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //房间号变为20
    self.room.no = 20;
}

@end

//Person.m 文件
#import "Person.h"

@implementation Person

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"no"]) {
        NSLog(@"Person 检测到 Room 的属性: %@ 值改变: %@", keyPath, change);
    }
}

//移除观察者对象,防止内存泄漏
- (void)dealloc{
    [self.room removeObserver:self forKeyPath:@"no"];
}
@end

运行,我们得到:
结果

KVO 的内部实现

下面我们分析下,KVO 的内部实现:
1> KVO 是基于 runtime 的 isa-swizzing 机制实现的;
2> 当类 A 的对象第一次被观察的时候,系统会在运行期动态创建类A 的派生类。系统命名为NSKVONotifying_A。
3> 在派生类 NSKVONotifying_A 中重写类 A 的setter方法,NSKVONotifying_A类在被重写的setter方法中实现通知机制。
4> 其中键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后,observeValueForKey:ofObject:change:context: 会被调用,继而 didChangeValueForKey: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了(后面介绍)。
5> 类 NSKVONotifying_A会重写 class方法,将自己伪装成类A。类 NSKVONotifying_A 还会重写 deallo 方法来释放资源。
系统将所有指向类 A 对象的isa指针指向类 NSKVONotifying_A 的对象。

为了证明上述过程:我们第一步注释掉ViewController 添加观察者的代码,在运行的时候,查看类 Room 的 isa 指针的值:
这里写图片描述

当将添加观察者处的代码打开,我们观察到,在运行的时候,Room 的 isa指针指向了NSKVONotifying_Room 类(派生类)
这里写图片描述

KVO 手动实现

在 Room.m 文件中实现:

/**
首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;

其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
*/
#import "Room.h"

@implementation Room

- (void)setNo:(int)no{
    [self willChangeValueForKey:@"no"];
    _no = no;
    [self didChangeValueForKey:@"no"];
}

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"no"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
@end

当我们再次运行观察,发现 Room 的 isa 指针指向Room类:
这里写图片描述

参考:

http://www.cppblog.com/kesalin/archive/2012/11/17/kvo.html

申明

以上观点,属于个人的理解,如果错误之处,欢迎拍砖。

原文地址:https://www.cnblogs.com/xiaocai-ios/p/7779759.html