iOS-底层探索19:KVO原理

iOS 底层探索 文章汇总

目录

  • 一、什么是KVO
  • 二、KVO基本使用
  • 三、KVO实现原理
  • 四、总结
  • 参考


一、什么是KVO

KVO是基于KVC的,全称是Key-Value-Observer键值观察者。KVO提供一种机制,指定一个被观察的对象(A类),当对象某个属性(A中的属性name)发生更改时,对象会获得通知,并作出相应处理;【且不需要给被观察的对象添加任何额外代码,就能使用KVO机制】。KVOObjective-C对观察者设计模式的一种实现。

KVOMVC设计架构下的项目很适合实现Mode模型和View视图之间的通讯。
例如:代码中,在模型类M创建属性数据,在控制器中创建观察者,一旦属性数据发生改变观察者就会收到通知,然后刷新相应的视图。

官方文档:Key-Value Observing Programming Guide

二、KVO基本使用

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.person = [NAPerson new];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    self.person.name = @"differ";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    CJLog(@"%@",change[NSKeyValueChangeNewKey]);
}

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name" context:NULL];
}
1、context的作用

当前观察者观察了多个对象,当这些对象中有同名的KeyPath时可以用来区分是哪个context下的KeyPath。eg:

static void *PersonNameContext = &PersonNameContext;
static void *AnimalNameContext = &AnimalNameContext;

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[self.animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:AnimalNameContext];
2、是否有必要移除观察者

有必要。官方文档提示如果没移除观察者在某些情况下会出现NSRangeException异常崩溃。

3、手动开启KVO

默认情况下KVO都是自动开启的,但是我们可实现手动开关KVO

@implementation NAPerson

// 自动开关
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

@end
4、集合类型的观察
[self.person addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew context:NULL];

//[self.person.dataArray addObject:@"1"];不起作用
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"1"];


三、KVO实现原理

@interface NAPerson : NSObject {
    @public
    NSString *_nickName;
}
@end

// ViewController
[self.person addObserver:self forKeyPath:@"_nickName" options:NSKeyValueObservingOptionNew context:NULL];

self.person->_nickName = @"differ";

[self.person removeObserver:self forKeyPath:@"_nickName" context:NULL];

通过对成员变量_nickName添加KVO观察后可以发现:KVO不会对成员变量进行观察,只对属性进行观察。由此可猜想KVO和属性的setter进行了关联。
通过官方文档可知,KVO会产生一个中间类,将观察对象的isa指向了这个中间类。断点调试如下:

iOS-底层探索19:KVO原理_第1张图片
KVO产生的中间类是分类还是派生子类?

修改代码遍历KVO观察对象的类以及子类

- (void)viewDidLoad {
    [super viewDidLoad];

    [self printClasses:[NAPerson class]];
    self.person = [NAPerson new];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self printClasses:[NAPerson class]];
}

#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls {
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i
iOS-底层探索19:KVO原理_第2张图片

从控制台输出的情况可以看出KVO生成的中间类是别被观察对象类的派生子类

KVO产生的中间类是继承还是重写属性的setter方法

打印出NSKVONotifying_NAPerson类中的方法

[self printClassAllMethod:objc_getClass("NSKVONotifying_NAPerson")];

#pragma mark - 遍历当前类的方法
- (void)printClassAllMethod:(Class)cls {
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i

由于NSKVONotifying_NAPerson类中存在setName方法,因此KVO子类重写了观察属性的setter方法。

对象的isa什么时候指回NAPerson

在移除观察者前后断点打印类名可知移除观察者后被观察对象的isa指回了原类

iOS-底层探索19:KVO原理_第3张图片

尽管移除了观察者,但是KVO产生的派生子类并不会移除,依然存在于内存中。因为当前界面销毁后通过上一个界面打印NAPerson类以及子类(printClasses:)依然会出现NSKVONotifying_NAPerson

KVO产生的派生子类重写的setter做了什么操作

添加断点:

iOS-底层探索19:KVO原理_第4张图片

修改name进入断点:

iOS-底层探索19:KVO原理_第5张图片

进入汇编中可以看到调用父类setName:方法前后调用了NSKeyValueWillChange、NSKeyValueDidChange

iOS-底层探索19:KVO原理_第6张图片


四、总结

基本原理:
  1. KVO是基于Runtime机制实现的
  2. 当某个类的对象属性第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的setter方法内实现真正的通知机制
  3. 如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
  4. 每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统就会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值是执行的是派生类的setter方法
  5. 键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:didChangeValueForKey:,在一个被观察属性发生改变之前,willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而observeValueForKey:ofObject:change:context:也会被调用

关于KVO中间类,有如下说明:

  • 实例对象isa的指向在注册KVO观察者之后,由原有类更改为指向中间类
  • 中间类重写了观察属性的setter方法、class、dealloc、_isKVOA方法
  • dealloc方法中,移除KVO观察者之后,实例对象isa指向由中间类更改为原类
  • 中间类从创建后,就一直存在内存中,不会被销毁
KVO为子类的观察者属性重写的setter方法的工作原理相当于:
- ( void)setName:( NSString *)name {
  [self willChangeValueForKey: @"name"];

  [super setValue:name forKey: @"name"]; //调用父类的存取方法 

  [self didChangeValueForKey: @"name"];
}
特点:
  • 观察者观察的是属性,只有遵循KVO变更属性值的方式才会执行KVO的回调方法,例如是否执行了setter方法、或者是否使用了KVC赋值。

  • 如果赋值没有通过setter方法或者KVC,而是直接修改属性对应的成员变量,例如:仅调用_name = @"newName",这时是不会触发KVO机制,更加不会调用回调方法的。

  • 所以使用KVO机制的前提是遵循KVO的属性赋值方式来变更属性值。


参考

Advancements in the Objective-C runtime
ro、rw、rwe介绍:iOS-底层探索13:分类的加载
KVO实现原理和具体应用

你可能感兴趣的:(iOS-底层探索19:KVO原理)