官方文档地址:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html
基本用法:
1. [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL]
** context作用,用来打标签在回调方法中做区分,应用场景是在如果父类和子类有同一个属性名,回调判断会复杂,如果是用context来区分会很方便,在内存中只能惟一的写一个,相当于一个静态值。而且查找更优越,如果判断类和属性列表的话需要查找他们的缓存列表,所以context的优点是嵌套少,性能好。
2. 改变值 self.person.name = @"person";
3. 响应回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary
NSLog(@"%@",change);
NSLog(@"downloadProgress == %@",self.person.downloadProgress);
}
4. 析构:
- (void)dealloc{
[self.student removeObserver:self forKeyPath:@"name"];
}
** 如果没有在dealloc中移除观察者,并且被观察对象是一个单例的换,再次打开界面重新给name复制时会闪退或者调用两次等异常。(所以一定要移除,至于是什么导致的崩溃,闭源源码无法看到,猜测是不断添加注册观察者,当系统需要给观察者复制是,原页面已经销毁,找不到原地址,导致野指针崩溃。),下面是官方文档:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if([key isEqualToString:@"name"]){
return YES;
}
return NO;
}
返回NO,则之前添加的观察者,不再回调,也可以选择观察一些特定的key.
如果这里返回NO,也可以使用在set方法里面添加手动观察方法的方式实现添加观察者:
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
KVO的一个简单应用(为downloadProgress添加观察者,影响下载进度的影响因素包括总的大小和已经写入的大小):
如果是传统的做法需要同时监控两个值,但是使用下面的方法就更便捷一些:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
上面的代码的主要作用是,当观察的值为downloadProgress时,添加连个影响因子totalData,writtenData,这样当这两个值改变的时候也会回调观察者方法。(联动观察 )
或者是把对象当做观察对象时,对象的属性改变也会调用观察者回调方法。
集合类观察:
如果采用上面的方式观察数组时,如果给数组添加元素,会发现不会调用观察者回调。如果要了解这个原因,就需要了解KVC的内容。(因为集合和简单变量的取值和赋值过程是不一样的)
[[self.student mutableArrayValueForKey:@"dataArray"] addObject:@"hello"];(满足KVC的设置流程)
这样就可以了,必须先初始化数组,接下来分析下原因:
根据之前KVC的官方文档中介绍。self.student是有可能获取不到值的,因为有_key,isKey,key这种变量的存在,赋值的时候不一定会复制到key中,有可能是_key中,所以self.student中不一定有值,所以获取不到。这样的话就需要直接避免这种情况,直接用KVC获取dataArray,这样可以规避KVC查找值导致的问题。
KVO底层原理:
首先验证下是否是观察的set方法:
为类添加一个属性(属性会自动生成set方法)和成员变量
------------------------------------------
#import "KVOPerson.h"
NS_ASSUME_NONNULL_BEGIN
@interface KVOStudent : KVOPerson{
@public
NSString *age;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int progress;
@property (nonatomic, assign) int total;
@property (nonatomic, assign) int download;
@property (nonatomic, strong) NSMutableArray *dataArray;
@end
------------------------------------------
NS_ASSUME_NONNULL_END
[self.student addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
[self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.student.name = @"fanxing";
self.student->age = @"age";
------------------------------------------
只打印了name,没有打印age,证明确实是KVO默认在set方法里处理的相关逻辑。下面是官方文档:
翻译:自动化KVO是由一种叫做isa-swizzing(isa改变类的指向)的技术实现的 :
首先使用objc_getClass()方法获取:(打印发现并没有中间类生成)
原因是应该查看对象的class而不是类的class,而且类对象的isa指向原类.
说明类是没有变化的,变化的是当前的对象的isa(原来是指向KVOStudent,现在指向 NSKVONotifying_KVOStudent )动态类
这个类是什么时候生成的呢?和原来的类KVOStudent是什么关系?
打印类的子类父类
#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
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
发现多了一个类,并且NSKVONotifying_KVOStudent是KVOStudent的子类,继承于KVOStudent。
接下来看下NSKVONotifying_KVOStudent 这个类的其他东西有没有不一样的地方,查看methodList
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
可以看到如果重写父类的方法,IMP实现的指针内存地址是不一样的
总结KVO做了哪些事情:
1.. 修改了isa的指向。
2. 从打印的内容来看,因为NSKVONotifying_KVOStudent是student的子类,并且setName的imp实现指针发生了变化,所以可以肯定的是,NSKVONotifying_KVOStudent类重写了setName方法。
3. 添加了一个class方法。
4. 重写了dealooc析构函数。
5. 添加了一个标记 _isKVO标识,标记是KVO的类。