一、KVO 简介
KVO 的全称是 Key-Value Observing,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发 KVO 的监听方法来通知观察者。KVO 是在 MVC 应用程序中的各层之间进行通信的一种特别有用的技术。
KVO 和 NSNotification
都是 iOS 中观察者模式的一种实现。
KVO 可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey:
等可变代理方法获得集合代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArray
和 NSSet
。
二、使用
先创建一个类,作为要监听的对象。
#import
NS_ASSUME_NONNULL_BEGIN
@interface DJModel : NSObject
@property (nonatomic, strong) NSString *name;
@end
NS_ASSUME_NONNULL_END
#import "DJModel.h"
@implementation DJModel
@end
监听实现
#import "ViewController.h"
#import "DJModel.h"
@interface ViewController ()
@property(nonatomic,strong)DJModel *model;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
[self.model addObserver:self forKeyPath:@"name" options:options context:@"context"];
self.model.name = @"123";
}
-(DJModel *)model{
if (!_model) {
_model = [[DJModel alloc]init];
}
return _model;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"keyPath:%@",keyPath);
NSLog(@"object:%@",object);
NSLog(@"change:%@",change);
NSLog(@"context:%@",context);
}
- (void)dealloc {
[self.model removeObserver:self forKeyPath:@"name"];
}
@end
1.实际应用
KVO 主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用 KVO 实现最为合适。斯坦福大学的 iOS 教程中有一个很经典的案例,通过 KVO 在 Model 和 Controller 之间进行通信。如图所示:
2.触发监听方法的方式
KVO 触发分为自动触发和手动触发两种方式。
(1)自动触发
如果是监听对象特定属性值的改变,通过以下方式改变属性值会触发 KVO:
- 使用点语法
- 使用
setter
方法 - 使用 KVC 的
setValue:forKey:
方法 - 使用 KVC 的
setValue:forKeyPath:
方法
如果是监听集合对象的改变,需要通过 KVC 的 mutableArrayValueForKey:
等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO。集合对象包含 NSArray
和 NSSet
。
(2)手动触发
普通对象属性或是成员变量使用:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
NSArray
对象使用:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
NSSet
对象使用:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
三、进阶使用
1.observationInfo
属性
observationInfo
属性是 NSKeyValueObserving.h
文件中系统通过分类给 NSObject 添加的属性,所以所有继承于 NSObject 的对象都含有该属性;
可以通过 observationInfo
属性查看被观察对象的全部观察信息,包括 observer
、keyPath
、options
、context
等。
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
2.context
的使用
注册方法 addObserver:forKeyPath:options:context:
中的 context
可以传入任意数据,并且可以在监听方法中接收到这个数据。
context
作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、 多监听;也可以用来传值。
KVO 只有一个监听回调方法 observeValueForKeyPath:ofObject:change:context:
,我们通常情况下可以在注册方法中指定 context
为 NULL
,并在监听方法中通过 object
和 keyPath
来判断触发 KVO 的来源。
但是如果存在继承的情况,比如现在有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对象的 name 属性进行观察。问题:
当 name 发生改变时,应该由谁来处理呢?
如果都由 person 来处理,那么在 Person 类的监听方法中又该怎么判断是自己的事务还是子类对象的事务呢?
这时候通过使用 context
就可以很好地解决这个问题,在注册方法中为 context
设置一个独一无二的值,然后在监听方法中对 context
值进行检验即可。
苹果的推荐用法:用 context
来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为 context
的值。可以为整个类设置一个 context
,然后在监听方法中通过 object
和 keyPath
来确定被观察属性,这样存在继承的情况就可以通过 context
来判断;也可以为每个被观察对象属性设置不同的 context
,这样使用 context
就可以精确的确定被观察对象属性。
context
优点:嵌套少、性能高、更安全、扩展性强。
context
注意点:
- 如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问
context
就可能导致 Crash; - 空传
NULL
而不应该传nil
。
3.监听集合对象
KVO 可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey:
等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArray
和 NSSet
。(注意:如果直接对集合对象进行操作改变,不会触发 KVO。)
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
self.person.mArray = [NSMutableArray arrayWithCapacity:5];
[self.person addObserver:self forKeyPath:@"mArray" options:(NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld) context:NULL];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// [self.person.mArray addObject:@"2"]; //如果直接对数组进行操作,不会触发KVO
NSMutableArray *array = [self.person mutableArrayValueForKey:@"mArray"];
[array addObject:@"1"];
[array replaceObjectAtIndex:0 withObject:@"2"];
[array removeObjectAtIndex:0];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/* change 字典的值为:
{
indexes:对应的值为数组操作的详细信息,包括索引等
kind: 对应的值为数组操作的方式:
2:代表插入操作
3:代表删除操作
4:代表替换操作
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
new/old:如果是插入操作,则字典中只会有new字段,对应的值为插入的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew)
如果是删除操作,则字典中只会有old字段,对应的值为删除的元素,前提条件是options中传入了(NSKeyValueObservingOptionOld)
如果是替换操作,则字典中new和old字段都可以存在,对应的值为替换后的元素和替换前的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
indexes = "<_NSCachedIndexSet: 0x600001d092e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
1
);
}
*/
NSLog(@"%@",change);
}
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"mArray"];
}
4.自动触发控制
可以在被观察对象的类中重写 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
方法来控制 KVO 的自动触发。
如果我们只允许外界观察 person 的 name 属性,可以在 Person 类如下操作。这样外界就只能观察 name 属性,即使外界注册了对 person 对象其它属性的监听,那么在属性发生改变时也不会触发 KVO。
// 返回值代表允不允许触发 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"name"]) {
automatic = YES;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
也可以实现遵循命名规则为 + (BOOL)automaticallyNotifiesObserversOf
的方法来单一控制属性的 KVO 自动触发,
+ (BOOL)automaticallyNotifiesObserversOfName
{
return NO;
}
- 第一个方法的优先级高于第二个方法。如果实现了
automaticallyNotifiesObserversForKey:
方法,并对做了处理,则系统就不会再调用该 的 automaticallyNotifiesObserversOf
方法。 -
options
指定的NSKeyValueObservingOptionInitial
触发的 KVO 通知,是无法被automaticallyNotifiesObserversForKey:
阻止的。
5.手动触发
使用场景:
使用 KVO 监听成员变量值的改变;
在某些需要控制监听过程的场景下。比如:为了尽量减少不必要的触发通知操作,或者当多个更改同时具备的时候才调用属性改变的监听方法。
由于 KVO 的本质,重写 setter
方法来达到可以通知所有观察者对象的目的,所以只有通过 setter
方法或 KVC 方法去修改属性变量值的时候,才会触发 KVO,直接修改成员变量不会触发 KVO。
当我们要使用 KVO 监听成员变量值改变的时候,可以通过在为成员变量赋值的前后手动调用 willChangeValueForKey:
和 didChangeValueForKey:
两个方法来手动触发 KVO,如:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[self.person willChangeValueForKey:@"age"];
self.person->_age = 18;
[self.person didChangeValueForKey:@"age"];
}
NSKeyValueObservingOptionPrior
(分别在值改变前后触发方法,即一次修改有两次触发)的两次触发分别在 willChangeValueForKey:
和 didChangeValueForKey:
的时候进行的。
如果注册方法中 options
传入 NSKeyValueObservingOptionPrior
,那么可以通过只调用 willChangeValueForKey:
来触发改变前的那次 KVO,可以用于在属性值即将更改前做一些操作。
6.新旧值相等时不触发
有时候我们可能会有这样的需求,KVO 监听的属性值修改前后相等的时候,不触发 KVO 的监听方法,可以结合 KVO 的自动触发控制和手动触发来实现。
例如:对 person 对象的 name 属性注册了 KVO 监听,我们希望在对 name 属性赋值时做一个判断,如果新值和旧值相等,则不触发 KVO,可以在 Person 类中如下这样实现,将 name 属性值改变的 KVO 触发方式由自动触发改为手动触发。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = YES;
if ([key isEqualToString:@"name"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)setName:(NSString *)name
{
if (![_name isEqualToString:name]) {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
7.手动观察集合属性
有些情况下我们想手动观察集合属性,下面以观察数组为例。
关键方法:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
需要注意的是,根据 KVC 的 NSMutableArray
搜索模式:
- 至少要实现一个插入和一个删除方法,否则不会触发 KVO。如
插入方法:insertObject:in
AtIndex:或insert :atIndexes:
删除方法:removeObjectFrom
AtIndex:或remove AtIndexes: - 可以不实现替换方法,但是如果不实现替换方法,执行替换操作时,KVO 会把它当成先删除后添加,即会触发两次 KVO。第一次触发的 KVO 中
change
字典的old
键的值为替换前的元素,第二次触发的 KVO 中change
字典的new
键的值为替换后的元素,前提条件是注册方法中的options
传入对应的枚举值。 - 如果实现替换方法,则执行替换操作只会触发一次 KVO,并且
change
字典会同时包含new
和old
,前提条件是注册方法中的options
传入对应的枚举值。
替换方法:replaceObjectIn
或AtIndex:withObject: replace
AtIndexes:with :
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"mArray"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)insertMArray:(NSArray *)array atIndexes:(NSIndexSet *)indexes
{
[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray insertObjects:array atIndexes:indexes];
[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
}
- (void)removeMArrayAtIndexes:(NSIndexSet *)indexes
{
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray removeObjectsAtIndexes:indexes];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
}
- (void)replaceMArrayAtIndexes:(NSIndexSet *)indexes withMArray:(NSArray *)array
{
[self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray replaceObjectsAtIndexes:indexes withObjects:array];
[self didChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
}
8.依赖观察
(1)一对一关系
有些情况下,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会跟着改变。
比如我们想要对 Download 类中的 downloadProgress 属性进行 KVO 监听,该属性的改变依赖于 writtenData 和 totalData 属性的改变。观察者监听了 downloadProgress ,当 writtenData 和 totalData 属性值改变时,观察者也应该被通知。以下有两种方法可以解决这个问题。
- 重写以下方法来指明 downloadProgress 属性依赖于 writtenData 和 totalData:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"writtenData",@"totalData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- 实现一个遵循命名规则为
keyPathsForValuesAffecting
的类方法,是依赖于其他值的属性名(首字母大写):
+ (NSSet *)keyPathsForValuesAffectingDownloadProgress
{
return [NSSet setWithObjects:@"writtenData",@"totalData", nil];
}
以上两个方法可以同时存在,且都会调用,但是最终结果会以 keyPathsForValuesAffectingValueForKey:
为准。
(2)一对多关系
以上方法在观察集合属性时就不管用了。例如,假如你有一个 Department 类,它有一个装有 Employee 类的实例对象的数组,Employee 类有 salary 属性。你希望 Department 类有一个 totalSalary 属性来计算所有员工的薪水,也就是在这个关系中 Department 的 totalSalary 依赖于所有 Employee 实例对象的 salary 属性。以下有两种方法可以解决这个问题。
- 你可以用 KVO 将 parent(比如 Department )作为所有 children(比如 Employee )相关属性的观察者。你必须在把 child 添加或删除到 parent 时把 parent 作为 child 的观察者添加或删除。在
observeValueForKeyPath:ofObject:change:context:
方法中我们可以针对被依赖项的变更来更新依赖项的值:
#import "Department.h"
static void *totalSalaryContext = &totalSalaryContext;
@interface Department ()
@property (nonatomic,strong)NSArray *employees;
@property (nonatomic,strong)NSNumber *totalSalary;
@end
@implementation Department
- (instancetype)initWithEmployees:(NSArray *)employees
{
self = [super init];
if (self) {
self.employees = [employees copy];
for (Employee *em in self.employees) {
[em addObserver:self forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:totalSalaryContext];
}
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)setTotalSalary:(NSNumber *)totalSalary
{
if (_totalSalary != totalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = totalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (void)dealloc
{
for (Employee *em in self.employees) {
[em removeObserver:self forKeyPath:@"salary" context:totalSalaryContext];
}
}
@end
- 使用 iOS 中观察者模式的另一种实现方式:通知(
NSNotification
)。
四、使用注意
1.移除观察者的注意点
- 在调用 KVO 注册方法后,KVO 并不会对观察者进行强引用,所以需要注意观察者的生命周期。至少需要在观察者销毁之前,调用 KVO 移除方法移除观察者,否则如果在观察者被释放后,再次触发 KVO 监听方法就会导致 Crash。
- KVO 的注册方法和移除方法应该是成对的,如果重复调用移除方法,就会抛出异常
NSRangeException
并导致程序 Crash。 - 苹果官方推荐的方式是,在观察者初始化期间(
init
或者viewDidLoad
的时候)注册为观察者,在释放过程中(dealloc
时)调用移除方法,这样可以保证它们是成对出现的,是一种比较理想的使用方式。
2.防止多次注册和移除相同的 KVO
有时候我们难以避免多次注册和移除相同的 KVO,或者移除了一个未注册的观察者,从而产生可能会导致 Crash 的风险。
三种解决方案:黑科技防止多次添加删除KVO出现的问题
- 利用
@try @catch
(只能针对删除多次KVO
的情况下) 给NSObject
增加一个分类,然后利用Runtime API
交换系统的removeObserver
方法,在里面添加@try @catch
; - 利用 模型数组 进行存储记录;
- 利用
observationInfo
里私有属性。
3.其它注意点
- 如果对象被注册成为观察者,则该对象必须能响应监听方法,即该对象所属类中必须实现监听方法。当被观察对象属性发生改变时就会调用监听方法。如果没有实现就会导致 Crash。所以 KVO 三部曲缺一不可。
-
keyPath
传入的是一个字符串,为避免写错,可以使用NSStringFromSelector(@selector(propertyName))
,将属性的getter
方法SEL
转换成字符串,在编译阶段对keyPath
进行检验。 - 如果注册方法中
context
传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context
就可能导致 Crash。 - 如果是监听集合对象的改变,需要通过 KVC 的
mutableArrayValueForKey:
等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO。如果直接对集合对象进行操作改变,不会触发 KVO。 - 在观察者类的监听方法中,应该为无法识别的
context
或者object
、keyPath
调用父类的实现[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
。
五.KVO原理
1.NSKVONotifying_
我们在对象添加监听之前分别打印对象类型
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"监听前model类型object_getClass:%@",object_getClass(self.model));
NSLog(@"监听前model类型class:%@",self.model.class);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
[self.model addObserver:self forKeyPath:@"name" options:options context:@"context"];
NSLog(@"监听后model类型object_getClass:%@",object_getClass(self.model));
NSLog(@"监听后model类型class:%@",self.model.class);
}
//打印结果
2021-12-21 10:21:39.709506+0800 DJTestTwo[6650:443575] 监听前model类型object_getClass:DJModel
2021-12-21 10:21:39.709661+0800 DJTestTwo[6650:443575] 监听前model类型class:DJModel
2021-12-21 10:21:39.710039+0800 DJTestTwo[6650:443575] 监听后model类型object_getClass:NSKVONotifying_DJModel
2021-12-21 10:21:39.710171+0800 DJTestTwo[6650:443575] 监听后model类型class:DJModel
我们看到,添加监听后,使用 object_getClass
方法获取model类型时获取到的是 NSKVONotifying_DJModel
。
这里就产生了几个问题:
- 为什么添加监听后使用
object_getClass
获取到的对象类型是NSKVONotifying_DJModel
?
我们获取添加监听后的model对象的类对象的父类
- (void)viewDidLoad {
[super viewDidLoad];
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
[self.model addObserver:self forKeyPath:@"name" options:options context:@"context"];
Class class = object_getClass(self.model);
Class superClass = class_getSuperclass(class);
NSLog(@"添加监听后model的类对象:%@",class);
NSLog(@"%@的父类对象%@",class,superClass);
}
//打印结果
2021-12-21 10:32:50.040556+0800 DJTestTwo[6759:450098] 添加监听后model的类对象:NSKVONotifying_DJModel
2021-12-21 10:32:50.040773+0800 DJTestTwo[6759:450098] NSKVONotifying_DJModel的父类对象DJModel
从打印结果可以看出,NSKVONotifying_DJModel
是 DJModel
的子类,说明我们添加了监听之后动态创建了一个 DJModel
的子类 NSKVONotifying_DJModel
,并将对象 DJModel
的类型更改为了 NSKVONotifying_DJModel
。
- 为什么添加监听收使用
class
方法和object_getClass
方法获取到的类型不一样?
我们查看class
和object_getClass
的源码
此源码在runtim源码的Object.mm中
-(id)class {
return (id)isa;
}
+ (id)class {
return self;
}
此源码在runtim源码的objc-class.mm中
Class object_getClass(id obj) {
if (obj) return obj->getIsa();
else return Nil;
}
我们从源码看出,实例对象调用 class
方法会返回 isa
指针,类对象调用 class
方法会返回自己,通过 object_getClass
方法获取对象的类型也会返回 isa
指针。从源码上看model对象添加监听之后使用 class
和使用 object_getClass
方法获取到的类型应该是一样的,但是这里却不同,我们猜测在添加了监听之后在 NSKVONotifying_DJModel
中重写了 class
方法。
我们打印一下添加监听前后 class
方法的 IMP
地址来确认是否重写了class
方法。
- (void)viewDidLoad {
[super viewDidLoad];
Class class1 = object_getClass(self.model);
NSLog(@"监听前model类型object_getClass:%@",object_getClass(class1));
NSLog(@"监听前model的class实现地址:%p",method_getImplementation(class_getInstanceMethod(class1, @selector(class))));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
[self.model addObserver:self forKeyPath:@"name" options:options context:@"context"];
Class class2 = object_getClass(self.model);
NSLog(@"监听前model类型object_getClass:%@",object_getClass(class2));
NSLog(@"监听前model的class实现地址:%p",method_getImplementation(class_getInstanceMethod(class2, @selector(class))));
}
//打印结果
2021-12-21 10:43:36.024516+0800 DJTestTwo[6825:456577] 监听前model类型object_getClass:DJModel
2021-12-21 10:43:36.024689+0800 DJTestTwo[6825:456577] 监听前model的class实现地址:0x10e267c9b
2021-12-21 10:43:36.025042+0800 DJTestTwo[6825:456577] 监听前model类型object_getClass:NSKVONotifying_DJModel
2021-12-21 10:43:36.025145+0800 DJTestTwo[6825:456577] 监听前model的class实现地址:0x10dd4e662
从打印结果可以看出,添加监听之后 class
方法的地址改变了,这验证了我们之前的猜想,NSKVONotifying_DJModel
类中重写了 class
方法。
我们监听对象时调用了 set
方法,我们对监听前后的 set
方法单独分析。
我们再添加监听前后分别打印 setName
方法的 IMP
地址。
- (void)viewDidLoad {
[super viewDidLoad];
Class class1 = object_getClass(self.model);
NSLog(@"监听前model类型object_getClass:%@",object_getClass(class1));
IMP imp1 = method_getImplementation(class_getInstanceMethod(class1, @selector(setName:)));
NSLog(@"监听前model的class实现地址:%p",imp1);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior;
[self.model addObserver:self forKeyPath:@"name" options:options context:@"context"];
Class class2 = object_getClass(self.model);
NSLog(@"监听后model类型object_getClass:%@",object_getClass(class2));
IMP imp2 = method_getImplementation(class_getInstanceMethod(class2, @selector(setName:)));
NSLog(@"监听后model的class实现地址:%p",imp2);
}
//打印结果
2021-12-21 10:48:53.412458+0800 DJTestTwo[6876:460439] 监听前model类型object_getClass:DJModel
2021-12-21 10:48:53.412632+0800 DJTestTwo[6876:460439] 监听前model的class实现地址:0x1023357b0
2021-12-21 10:48:53.413003+0800 DJTestTwo[6876:460439] 监听后model类型object_getClass:NSKVONotifying_DJModel
2021-12-21 10:48:53.413131+0800 DJTestTwo[6876:460439] 监听后model的class实现地址:0x102648b57
通过打印结果可以看出 setName
方法也在 NSKVONotifying_DJModel
中被重写了,我们再使用lldb来看下 setName
具体是什么
(lldb) print (IMP)0x10ba7b7b0
(IMP) $1 = 0x000000010ba7b7b0 (DJTestTwo`-[DJModel setName:] at DJModel.h:13)
(lldb) print (IMP)0x10bd8eb57
(IMP) $2 = 0x000000010bd8eb57 (Foundation`_NSSetObjectValueAndNotify)
第一个地址打印的是添加监听前 setName
方法的 IMP
地址,第二个打印的是添加监听后 setName
方法的 IMP
地址。
这里看出添加监听前 setName
对应的具体方法就是 setName
,但是添加监听后,setName
对应的鸡头方法却变成了 _NSSetObjectValueAndNotify
函数。
下面我们就来研究一下 _NSSetObjectValueAndNotify
函数。
2._NSSetObjectValueAndNotify
__NSSetBoolValueAndNotify
__NSSetCharValueAndNotify
__NSSetDoubleValueAndNotify
__NSSetFloatValueAndNotify
__NSSetIntValueAndNotify
__NSSetLongLongValueAndNotify
__NSSetLongValueAndNotify
__NSSetObjectValueAndNotify
__NSSetPointValueAndNotify
__NSSetRangeValueAndNotify
__NSSetRectValueAndNotify
__NSSetShortValueAndNotify
__NSSetSizeValueAndNotify
__NSSetUnsignedCharValueAndNotify
__NSSetUnsignedIntValueAndNotify
__NSSetUnsignedLongLongValueAndNotify
__NSSetUnsignedLongValueAndNotify
__NSSetUnsignedShortValueAndNotify
从上面与KVO相关的方法中我们可以看出,每一种数据类型都对应了一个 setXXXValueAndNotify
函数。
不过这些函数的具体实现没有公布,所以内部构造这里还是不清楚。
但是我们知道,在调用 `setXXXValueAndNotify 函数的过程中会调用另外两个方法。
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
测试后得出了以下几个结论:
- 如果在创建监听的时候只使用了
NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
这两个枚举值,那么observeValueForKeyPath
方法会在didChangeValueForKey
方法调用后被调用。 - 如果在创建监听的时候使用了
NSKeyValueObservingOptionPrior
枚举值,那么observeValueForKeyPath
方法会在willChangeValueForKey
方法调用后被调用第一次,在didChangeValueForKey
方法调用后被调用第二次。 - 如果在创建监听的时候使用了
NSKeyValueObservingOptionInitial
枚举值,那么在observeValueForKeyPath
方法会在willChangeValueForKey
方法调用之前被调用一次。
我们还可以利用这两个方法手动触发 observeValueForKeyPath
方法:
- 当使用了
NSKeyValueObservingOptionInitial
枚举值时,创建监听时就会调用一次observeValueForKeyPath
方法,不需要其他条件触发。 - 当使用
NSKeyValueObservingOptionPrior
枚举值时,手动调用willChangeValueForKey
时可以触发一次observeValueForKeyPath
方法的调用。 - 如果想在
didChangeValueForKey
方法调用后再调用一次observeValueForKeyPath
方法,需要同时实现willChangeValueForKey
和didChangeValueForKey
两个方法才行。
所以我们判断在 _NSSetObjectValueAndNotify
函数内部,在调用原来的 set
方法之前插入了 willChangeValueForKey
方法,在调用原来的 set
方法之后插入了 didChangeValueForKey
方法,并根据初始化时的枚举值决定调用 observeValueForKeyPath
的时机。
3.总结
(1)添加监听时,会动态创建一个监听对象类型的子类,并将监听对象的 isa
指针指向新的子类。
(2)子类中重写了 class
和监听属性的 set
方法。
(3)重写 class
方法是为了不将动态创建的类型暴露出来。
(4)重写 set
方法是将 set
方法的具体实现替换成了与属性类型相关的 __NSSetXXXValueAndNotify
函数。
(5)在 __NSSetXXXValueAndNotify
函数内部在 set
方法前后分别插入了 willChangeValueForKey
和 didChangeValueForKey
这两个方法。
(6)根据添加监听时的枚举值决定调用 observeValueForKeyPath
的具体时机。