一、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
指回父类。动态生成的子类并不会销毁。
网友评论