KVO概念
KVO ->Key-Value observing,键值观察,当被观察对象中指定属性发现变化时,观察者就可以得到通知,进而进行后续操作。
KVO使用
根据KVO官方文档 得知,正常使用大体分为以下流程:
- 注册观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
- KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",change[NSKeyValueChangeNewKey]);
}
}
- 移除观察者
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
context 用法及意义
根据官方文档解读可以得知:
context
即上下文
主要作用就是防止根据keypath
查询通知来源
时,因父类
、子类
观察到相同路径
而出现的问题,不同的keypath
创建不同的context
,这样就可以不用通过字符串比较
的方式去确定keypath
,从而提高性能以及代码可读性
。
//定义context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
//注册观察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
//KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
NSLog(@"%@",change);
}else if (context == PersonNameContext){
NSLog(@"%@",change);
}
}
观察者移除
根据官方文档可以得知:
如果没有注册而移除了观察者,时就会出现
NSRangeException
,如果进行了一次addObserver
,则对应的也需要removeObserver
释放后,观察者不会自动将其自身移除。被观察对象继续发送通知,而忽略了观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。因此,您可以确保观察者在从内存中消失之前将自己删除。
该协议无法询问对象是观察者还是被观察者。构造代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间(例如,在
init或viewDidLoad
中)注册为观察者,并在释放过程中(通常在dealloc
中)注销,以确保成对和有序地添加和删除消息,并确保观察者在注册之前被取消注册,从内存中释放出来
总结:
KVO
注册观察者(addObserver
)与移除观察者(removeObserver
)是成对出现的,如果只注册,不移除
,则会出现野指针
类型的崩溃,如果只移除,不注册
,则会NSRangeException
KVO 的自动与手动触发
系统默认的事自动触发
,即如果添加了观察者
,并且回调方法
中存在相应的处理,这时只要属性值发生改变
,就会调用,如果关闭
自动触发automaticallyNotifiesObserversForKey设置为NO
,这时就需要将需要观察的属性改变前增加willChangeValueForKey
,改变后增加didChangeValueForKey
,这样就可以触发,通过手动触发能够更好地贴合项目中的需求,增加扩展性
KVO一对多
通过keyPathsForValuesAffectingValueForKey
方法将多个属性合并成一个进行观察,以下载进度为例
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
self.person.writtenData += 10;
self.person.totalData += 1;
[self.person removeObserver:self forKeyPath:@"downloadProgress"];
- (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];
}
KVO 键值变化类型
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//设值
NSKeyValueChangeInsertion = 2,//插入
NSKeyValueChangeRemoval = 3,//移除
NSKeyValueChangeReplacement = 4,//替换
};
//根据下面代码来实现不同的kind
self.student.name = [NSString stringWithFormat:@"%@+",self.student.name];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
[[self.person mutableArrayValueForKey:@"dateArray"] removeLastObject];
[[self.person mutableArrayValueForKey:@"dateArray"] replaceObjectAtIndex:0 withObject:@"3"];
- 注:当当前观察对象为
数组
时,不可以直接通过赋值的方式进行更改,而是需要mutableArrayValueForKey
或mutableArrayValueForKeyPath
进行获取,然后才能实现更改回调
KVO 底层探究
由于KVO没有对应的开源代码,故而通过跟流程的方式查看
- 观察的是setter方法
验证:根据属性
和成员变量
的区别可以得知,两者之间属性存在setter
方法,而成员变量没有
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"KC";
self.person->name = @"Cooci";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
根据上述结果可以得知,属性
nickName
更改成功收到回调,而成员变量name
没有收到回调,故而可以验证KVO
观察的是setter
方法
- 中间类
根据官方文档得知,在注册观察者之后,观察对象的isa
会发生改变
截屏2022-01-05 上午10.32.13.png
根据断点获取className
可以看出isa的指向
确实发生了改变
[self printClasses:[LGPerson class]];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
// [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClasses:[LGPerson class]];
[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
[self printClassAllMethod:[LGStudent class]];
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[I];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
#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<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[I]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
通过遍历方法和子类可以看出
- 在没有注册观察值之前,只存在
LGPerson
与LGStudent
两个类,而注册完成后,多出了一个NSKVONotifying_LGPerson
,故而可以确定,NSKVONotifying_LGPerson
是LGPerson
的子类 - 通过观察
NSKVONotifying_LGPerson
中的方法列表可以看出,子类中存在setter
方法,在LGStudent
中实现了setNickName
的重写,与NSKVONotifying_LGPerson
中一致,故而可以确定,在NSKVONotifying_LGPerson
实现了setter、class、dealloc、_isKVO
的重写
,而非继承
- 根据上图可以得知,在移除观察者之前isa指向的是子类
NSKVONotifying_LGPerson
,移除之后指回LGPerson
- 虽然观察者移除了,但是在其它页面查看
LGPerson
子类时可以发现中间类NSKVONotifying_LGPerson
并没有销毁
,这样可以避免每次进行创建而造成性能低下,通过重用的方式使得中间类一经创建就一直存在
KVO自定义实现
完整代码只是基本实现