一、kvo简介
Key-Value Observing Programming Guide
对于kvo使用分为3步:
- 1.
Registering as an Observer
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
observer:要添加的监听者对象,当监听的属性发生改变时会通知该对象,必须实现- observeValueForKeyPath:ofObject:change:context:方法,否则程序会抛出异常。
keyPath:监听的属性,不能传nil。
options:指明通知发出的时机以及change中的键值。
context:是一个可选的参数,可以传任何数据。
⚠️添加监听的方法
addObserver:forKeyPath:options:context:并不会对监听和被监听的对象以及context做强引用,必须自己保证他们在监听过程中不被释放。
- 2.
Receiving Notification of a Change
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- 3.
Removing an Object as an Observer
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
1.1 options
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,//更改前的值
NSKeyValueObservingOptionOld = 0x02,//更改后的值
NSKeyValueObservingOptionInitial = 0x04,//观察最初的值(在注册观察服务时会调用一次触发方法)
NSKeyValueObservingOptionPrior = 0x08 //分别在值修改前后触发方法(即一次修改有两次触发)
};
可以看到NSKeyValueObservingOptions有4个枚举值,测试代码如下:
self.obj = [HPObject alloc];
self.obj.name = @"hp1";
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.obj.name = @"hp2";
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"change:%@",change);
}
修改options参数输出如下:
//NSKeyValueObservingOptionNew
change:{
kind = 1;
new = hp2;
}
//NSKeyValueObservingOptionOld
change:{
kind = 1;
old = hp1;
}
//NSKeyValueObservingOptionInitial
change:{
kind = 1;
}
change:{
kind = 1;
}
//NSKeyValueObservingOptionPrior
change:{
kind = 1;
notificationIsPrior = 1;
}
change:{
kind = 1;
}
//NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior
change:{
kind = 1;
new = hp1;
}
change:{
kind = 1;
notificationIsPrior = 1;
old = hp1;
}
change:{
kind = 1;
new = hp2;
old = hp1;
}
// 0
change:{
kind = 1;
}
NSKeyValueChangeKey定义如下:
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey;
-
NSKeyValueChangeKindKey:指明了变更的类型,一般情况下返回的都是1。集合中的元素被插入,删除,替换时返回2、3、4。
NSKeyValueChange定义如下:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//普通类型设置
NSKeyValueChangeInsertion = 2,//集合元素插入
NSKeyValueChangeRemoval = 3,//集合元素移除
NSKeyValueChangeReplacement = 4,//集合元素替换
};
-
NSKeyValueChangeNewKey:改变后新值的key,如果是集合返回的数据是集合。 -
NSKeyValueChangeOldKey:改变前旧值的key,如果是集合返回的数据是集合。 -
NSKeyValueChangeIndexesKey:若果是集合类型,这个键的值是NSIndexSet对包含了增加,移除或者替换对象的index。 -
NSKeyValueChangeNotificationIsPriorKey:NSKeyValueObservingOptionPrior调用前标记。
综上:
-
NSKeyValueObservingOptionNew:指明change字典中应该包含改变后的新值。 -
NSKeyValueObservingOptionOld:指明change字典中应该包含改变前的旧值。 -
NSKeyValueObservingOptionInitial:注册后立马调用一次,这种通知只会发送一次。可以做一些一次性的工作。当同时指定new/old/initial的情况时,initial通知只包含new值。(实际上还是old值,因为是注册后立马调用,所以实际上对它来说是新值。任何情况下initial都不会包含old) -
NSKeyValueObservingOptionPrior:修改前后触发,会调用两次。修改前触发会包含notificationIsPrior字段。当同时指定new/old时,修改前会包含old,修改后会包含new和old。(一般的通知发出时机都是在属性改变后,虽然change字典中包含了old和new,但是通知还是在属性改变后才发出)。 -
0:直接传递0,在每次调用的时候都返回包含kind的change。可以理解为默认实现。
1.2 context
这个参数最后会被传递到监听者的响应方法中,可以用来区分不同通知,也可以用来传值。
对于多个keyPath的观察,需要在observeValueForKeyPath同时判断object与keyPath,可以声明一个静态变量传递给context用来区分不同的通知提高代码的可读性:
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
当然如果子类和父类都实现了对同一对象的同一属性的观察,并且父类和子类都可能对其进行设值,那么这个时候用context区分就很有用了。
1.3 移除观察者
官方文档说了在观察者dealloc的时候被观察者不会自动移除观察者,还是会继续给观察者发送消息。需要自己保证移除。
比如某个页面监听了一个对象的属性,这个对象是从前一个页面传递进来的(本质上是对象不被释放)。在不移除观察的情况下,多次进入这个页面在属性变化的时候就发生了crash:

根本原因是之前进入页面的时候观察者没有移除,导致发送消息的时候之前的
observer不存在。
kvo的使用三步曲要完整
当然如果页面是个单例则不会崩溃,如果
addObserver每次都调用则会进行多次回调。
二、kvo初探
2.1 kvo手动自动通知
在被观察者中实现automaticallyNotifiesObserversForKey可以控制kvo是否自动通知:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
NSLog(@"%s key:%@",__func__,key);
return NO;
}
- 当返回
NO的时候不会自动调用通知,当返回YES的时候会进行自动通知。 -
automaticallyNotifiesObserversForKey是在注册观察者的时候进行调用的。所以在中途通过开关配置是无效的(只在addObserver第一次调用的时候调用)。
image.png
willChangeValueForKey & didChangeValueForKey手动通知
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
- 可以通过
willChangeValueForKey与didChangeValueForKey进行手动通知。 - 手动通知
key可以自己写(key必须存在类中),所以这里可以做映射。将多个属性的变化映射到一个属性上。 - 手动通知不受自动开关状态的影响。
- 如果手动和自动同时开启,则两个都会发送通知。
2.2 嵌套层次的监听
keyPathsForValuesAffectingValueForKey(key - keys)
比如下载文件:
下载进度 = 已下载数据大小 / 总数据大小。总数据大小由于添加文件可能会发生变化。
@interface HPObject : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@end
@implementation HPObject
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSLog(@"%s key:%@",__func__,key);
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (NSString *)downloadProgress {
if (self.writtenData == 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
@end
监听和调用:
- (void)viewDidLoad {
[super viewDidLoad];
[self.obj addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"change:%@",change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.obj.writtenData += 10;
self.obj.totalData += 1;
}
- (void)dealloc {
[self.obj removeObserver:self forKeyPath:@"downloadProgress"];
NSLog(@"dealloc");
}
- 在
keyPathsForValuesAffectingValueForKey中对key进行了映射(只在addObserver第一次调用的时候调用)。 -
keyPathsForValuesAffectingValueForKey中会进行递归映射,也就是totalData和writtenData也会去查找自身的依赖。
+[HPObject keyPathsForValuesAffectingValueForKey:] key:downloadProgress
+[HPObject keyPathsForValuesAffectingValueForKey:] key:writtenData
+[HPObject keyPathsForValuesAffectingValueForKey:] key:writtenData
+[HPObject keyPathsForValuesAffectingValueForKey:] key:totalData
+[HPObject keyPathsForValuesAffectingValueForKey:] key:totalData
- 这个时候通过
touchesBegan中调用writtenData与totalData就能监听到downloadProgress的变化了。writtenData与totalData设置值的时候会调用到downloadProgress中(newValue取值)。所以只要有任一一个变化都会调用到observeValueForKeyPath中。 - 在首次(所有的第一次,不论页面是否重建与否,这里是与
keyPathsForValuesAffectingValueForKey次数对应的)touchesBegan时,observeValueForKeyPath在上面的案例中会调用3次。
image.png
可以看到确实是系统内部直接多调用了一次。
2.3 kvo对可变数组的观察
self.obj.dateArray = [NSMutableArray array];
[self.obj addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
[self.obj.dateArray addObject:@(1)];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"change:%@",change);
}
上面的案例dateArray添加元素并不能触发kvo,需要修改为:
[[self.obj mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
[[self.obj mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
[[self.obj mutableArrayValueForKey:@"dateArray"] removeObject:@"1"];
[self.obj mutableArrayValueForKey:@"dateArray"][0] = @"3";
输出:
change:{
indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
1
);
}
change:{
indexes = "<_NSCachedIndexSet: 0x600000a635c0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
2
);
}
change:{
indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 3;
}
change:{
indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 4;
new = (
3
);
}
- 通过
mutableArrayValueForKey获取dateArray再添加就能监听到了。 - 这个时候
kind变为了2、3、4就与前面介绍的NSKeyValueChange对应上了。
⚠️kvo监听集合元素变化,需要用到kvc的原理机制才能监听到变化。由于kvo底层也是由kvc实现的
集合相关
API如下:
NSMutableArray:mutableArrayValueForKey,mutableArrayValueForKeyPath。
NSMutableSet:mutableSetValueForKey,mutableSetValueForKeyPath。
NSMutableOrderedSet:mutableOrderedSetValueForKey,mutableOrderedSetValueForKeyPath。
These methods provide the additional benefit of maintaining key-value observing compliance for the objects held in the collection object
说明了集合类型的要特殊处理,具体可以参考kvc的说明:Accessing Collection Properties
2.3.1 可变数组专属API
当然除了上面对于集合类型的赋值通过kvc相关接口还可以通过数组专属API来完成。
@property (nonatomic, strong) NSMutableArray <HPObject *>*array;
self.array = [NSMutableArray array];
HPObject *obj1 = [HPObject alloc];
obj1.name = @"obj1";
[self.array addObject:obj1];
HPObject *obj2 = [HPObject alloc];
obj2.name = @"obj2";
[self.array addObject:obj2];
[self.array addObserver:self toObjectsAtIndexes:[NSIndexSet indexSetWithIndex:1] forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.array[0].name = @"_obj0";
self.array[1].name = @"_obj1";
输出:
change:{
kind = 1;
new = "_obj1";
}
这样当self.array[1].name发生变化的时候就监听到了。这里本质上就相当于是对obj1的监听。后续数组中替换了1位置的数组是监听不到的。
HPObject *obj3 = [HPObject alloc];
obj3.name = @"obj3";
self.array[1] = obj3;
self.array[1].name = @"_obj3";
这样替换后监听不到。
三、kvo原理分析
Key-Value Observing Implementation Details
根据官方文档可以看到使用了isa-swizzling技术。
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.
3.1 isa-swizzling验证
直接在addObserver的调用处打个断点:

在
addObserver后obj从HPObject变成了NSKVONotifying_HPObject。
- 那么
NSKVONotifying_HPObject是什么时候生成的呢?
重新运行,在addObserver之前验证NSKVONotifying_HPObject:
(lldb) p objc_getClass("NSKVONotifying_HPObject")
(Class _Nullable) $0 = nil
这样就意味着NSKVONotifying_HPObject是在addObserver的时候底层动态生成的。
-
NSKVONotifying_HPObject与HPObject有什么关系呢?
- (void)printClasses:(Class)cls {
//注册类总个数
int count = objc_getClassList(NULL, 0);
//先将类本身放入数组中
NSMutableArray *array = [NSMutableArray arrayWithObject:cls];
//开辟空间
Class *classes = (Class *)malloc(sizeof(Class)*count);
//获取已经注册的类
objc_getClassList(classes, count);
for (int i = 0; i < count; i++) {
//获取cls的子类,一层。
if (cls == class_getSuperclass(classes[i])) {
[array addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@",array);
}
上面这段代码是打印类以及它的子类(单层)。
调用:
[self printClasses:[HPObject class]];
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self printClasses:[HPObject class]];
[self printClasses:objc_getClass("NSKVONotifying_HPObject")];
输出:
classes = (
HPObject,
HPCat
)
classes = (
HPObject,
"NSKVONotifying_HPObject",
HPCat
)
classes = (
"NSKVONotifying_HPObject"
)
-
NSKVONotifying_HPObject是在addObserver过程中底层动态添加的。 -
NSKVONotifying_HPObject是HPObject的子类。NSKVONotifying_HPObject本身没有子类。
3.2 kvo 生成子类分析
既然NSKVONotifying_HPObject是HPObject的子类,那么它都有什么内容呢?
方法:
- (void)printClassAllProtocol:(Class)cls {
unsigned int count = 0;
Protocol * __unsafe_unretained _Nonnull * _Nullable protocolList = class_copyProtocolList(cls, &count);
for (int i = 0; i < count; i++) {
Protocol *proto = protocolList[i];
NSLog(@"%s",protocol_getName(proto));
}
free(protocolList);
}
输出:
setName:-0x7fff207bbb57
class-0x7fff207ba662
dealloc-0x7fff207ba40b
_isKVOA-0x7fff207ba403
同理可以获取协议,属性以及成员变量:
- (void)printClassAllProtocol:(Class)cls {
unsigned int count = 0;
Protocol * __unsafe_unretained _Nonnull * _Nullable protocolList = class_copyProtocolList(cls, &count);
for (int i = 0; i < count; i++) {
Protocol *proto = protocolList[i];
NSLog(@"%s",protocol_getName(proto));
}
free(protocolList);
}
- (void)printClassAllProprerty:(Class)cls {
unsigned int count = 0;
objc_property_t *propertyList = class_copyPropertyList(cls, &count);
for (int i = 0; i < count; i++) {
objc_property_t property = propertyList[i];
NSLog(@"%s-%s", property_getName(property), property_getAttributes(property));
}
free(propertyList);
}
- (void)printClassAllIvars:(Class)cls {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(cls, &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
NSLog(@"%s-%s",ivar_getName(ivar),ivar_getTypeEncoding(ivar));
}
free(ivarList);
}
没有输出任何内容。那么核心就在方法了。
_isKVOA很好理解用来判断是否kvo生成的类,class标记类型。setName:是对父类name的setter方法进行了重写。dealloc中进行了isa重新指回。
3.2.1 class
在addObserver后调用class输出:
(lldb) p self.obj.class
(Class) $0 = HPObject
那么重写class就是为了返回原来的类的信息。不会返回kvo类自己的class信息。
3.2.2 dealloc
既然NSKVONotifying_HPObject是动态创建的,那么它销毁吗?
在dealloc中removeObserver前后分别验证:

可以看到移除后
isa指回了原来的类,也就是dealloc中进行了isa的指回。并且NSKVONotifying_HPObject类仍然存在。
3.2.3 setter
既然重写了setName:观察属性,那么成员变量能观察么?增加age成员变量:
[self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.obj addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
self.obj->age = 18;
当对age进行赋值并没有触发回调。那么就说明了对setter方法进行的监听。
在dealloc中removeObserver后查看name的值:

那就说明在
kvo生成的类中对name的修改影响到了原始类。对
name下个内存断点:
(lldb) watchpoint set variable self->_obj->_name
Watchpoint created: Watchpoint 1: addr = 0x60000129b260 size = 8 state = enabled type = w
watchpoint spec = 'self->_obj->_name'
new value: 0x0000000000000000
在赋值的时候堆栈如下:
* thread #1, queue = 'com.apple.main-thread', stop reason = watchpoint 1
* frame #0: 0x00007fff2018b1e2 libobjc.A.dylib`objc_setProperty_nonatomic_copy + 44
frame #1: 0x000000010afecd70 KVODemo`-[HPObject setName:](self=0x000060000129b250, _cmd="setName:", name=@"HP") at HPObject.h:19:39
frame #2: 0x00007fff207c2749 Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 646
frame #3: 0x00007fff207c300b Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
frame #4: 0x00007fff207bbc64 Foundation`_NSSetObjectValueAndNotify + 269
frame #5: 0x000000010afed248 KVODemo`-[HPDetailViewController viewDidLoad](self=0x00007fe291e0d890, _cmd="viewDidLoad") at HPDetailViewController.m:43:14
调用逻辑如下:
-[HPObject setName:]
Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
Foundation`_NSSetObjectValueAndNotify
_NSSetObjectValueAndNotify汇编调用主要如下:
"willChangeValueForKey:"
call 0x7fff2094ff0e
"didChangeValueForKey:"
"_changeValueForKey:key:key:usingBlock:"
在_changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:中有获取observers操作:
_NSKeyValueObservationInfoGetObservances
那么意味着在处理完所有事情后会进行通知。
并且有NSKeyValueWillChange与NSKeyValueDidChange:

继续在observeValueForKeyPath的回调中打个断点:

确认是在
NSKeyValueNotifyObserver通知中进行的回调。
总结(kvo原理)
-
addObserver动态生成子类NSKVONotifying_XXX。- 重写
class方法,返回父类class信息。父类isa指向子类。
- 重写
- 给动态子类添加
setter方法(所有要观察的属性)。 - 消息转发给父类。
-
setter会调用父类原来的方法进行赋值,完成后进行回调通知。
-
- 移除
observer的时候isa指回父类。动态生成的子类并不会销毁。


