KVO概述
KVO,或者key-value observing,是可以对OC对象的属性进行观察,并在属性发生改变的的时候,发出通知的神奇魔法。之所以称之为神奇,是因为KVO是依赖Object-C运行时机制,而我们需要处理很少的工作,就可以发送和接受通知。
KVO的使用
KVO的使用步骤
1. 注册观察者,为观察者者添加注册的key
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)contex;
observer : 观察者
keyPath :需要观察的属性
-
option :需要观察的属性值的变化的选择项。
NSKeyValueObservingOptionInitial
: 如果需要在观察者注册了监听后立刻发送消息给observer,可以添加这个选择项。NSKeyValueObservingOptionNew
: 在回调中,获取属性改变之后的值。NSKeyValueObservingOptionOld
: 在回调方法中,获取属性改变之前的值。
context :可以认为是在回调方法中用来区分不同通知的来源的标识。
2. 实现KVO的回调方法
//监听的回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context;
keyPath: 被观察的属性值
object:被 观察的属性值所在的对象
-
change: 是一个字典值,可以在这个对象中取出改变前后的值
NSKeyValueChangeNewKey
NSKeyValueChangeOldKey
context: 用来区分不同通知的来源的标识,与添加观察者方法的context搭配使用,是同一个值。
3. 移除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
observer: 观察者
keyPath: 被观察的属性值
4. 代码
//定义 context
static void * M2_KVONameContext = &M2_KVONameContext;
//1、添加观察者
- (void)addObserver{
//添加观察者,使用context标识
[self.m2 addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:M2_KVONameContext];
}
//2、监听的回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == M2_KVONameContext){
if ([keyPath isEqual:@"name"]) {
NSLog(@"NSKeyValueObservering new is %@", change[NSKeyValueChangeNewKey]);
NSLog(@"NSKeyValueObservering old is %@", change[NSKeyValueChangeOldKey]);
}
}
}
- (void)dealloc{
//3、移除
[self.m2 removeObserver:self forKeyPath:@"name" context:M2_KVONameContext];
}
KVO能够观察到回调的方式
属性的点语法。例:
self.m2.name = @"我是测试数据";
KVC方式进行赋值。例:
[self.m2 setValue:@"我是测试数据" forKey:@"myName"];
-
直接操作成员变量的方法,是不会触发KVO回调的,这种情况下可以使用KVC的方法。
self.m2->pName = @"我是测试数据";
这种方式不会回调KVO[self.m2 setValue:@"我是测试数据" forKey:@"pName"];
是可以回调KVO的
KVO禁止自动通知观察者
在程序中,一个对象的属性注册了观察者,但是不希望获取到该属性的KVO回调。比如有一些关键信息,不希望三方进行观察,就可以使用下面这个方法进行处理。
方法:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
代码实例:
//禁止某个属性自动的通知观察者,例如关键信息不想让三方知道,就可以重写这个函数
//该方法默认发挥true
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqual:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
KVO手动触发
KVO的属性发生改变时,是系统自动通知给观察者。如果自己希望实现自定义传递消息的时机,可以使用下面的两个方法。
涉及的两个方法:
-(void)willChangeValueForKey:(NSString *)key
:在属性发生改变前调用- (void)didChangeValueForKey:(NSString *)key;
: 在属性发生改变后调用
把属性的key值作为参数,发送NSKeyValueChangeSetting
类型的通知消息,并把发送给每一个注册该key的观察者。
这两个方法的调用,必须始终成对进行。
代码实例,实现在属性值前后不一致的情况下发送消息,可以和automaticallyNotifiesObserversForKey
合作完成这个实例。
//手动触发
-(void)setName:(NSString *)name{
if (name != _name) {
//手动触发KVO
[self willChangeValueForKey:@"name"];
_name = name;
//手动触发KVO
[self didChangeValueForKey:@"name"];
}
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqual:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
可变数组的KVO通知
观察可变数组,可变数组实例化以后,添加元素的过程中,如果直接调用- (void)addObject:(ObjectType)anObject
方法是不会通知KVO回调的。需要使用KVO中的方法- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key
,然后再去调用addObject方法。
对象中添加爱可变数组
@interface SecondModel : NSObject
//添加可变数组
@property (nonatomic, strong) NSMutableArray * mutableArray;
@end
调用可变数组的添加方法
//实例化可变数组
self.m2.mutableArray = [NSMutableArray array];
//为其添加观察者
[self.m2 addObserver:self forKeyPath:@"mutableArray" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:M2_KVONameContext];
//直接调用addObject方法,不会触发KVO回调
[self.m2.mutableArray addObject:@"1"];
// 会触发KVO回调
[[self.m2 mutableArrayValueForKey:@"mutableArray"] addObject:@2];
KVC和KVO
如果对象的属性或者成员变量实现了KVO监听,使用KVC对属性或者成员变量进行赋值,都会产生KVO的通知消息,调起回调。
KVC是怎样实现的KVO回调的?下面我们添加一个成员变量myName
, 并实现其setter
、getter
方法,重写- (void)willChangeValueForKey:(NSString *)key
和-(void)didChangeValueForKey:(NSString *)key
两个方法。在控制器中对添加myName
的KVO监听,使用KVC进行赋值。
@interface SecondModel(){
NSString * myName;
}
@end
@implementation SecondModel
//setter
- (void)setMyName:(NSString*)myName{
NSLog(@"func is %@",NSStringFromSelector(_cmd));
self->myName = myName;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"forUndefinedKey is %@",key);
}
//getter
- (NSString *)myName{
NSLog(@"func is %@",NSStringFromSelector(_cmd));
return self->myName;
}
- (id)valueForUndefinedKey:(NSString *)key{
NSLog(@"valueForUndefinedKey is %@",key);
return @"valueForUndefinedKey";
}
//没有找到setter/getter的情况下,是否直接访问成员变量
+ (BOOL)accessInstanceVariablesDirectly{
return false;
}
- (void)willChangeValueForKey:(NSString *)key{
NSLog(@"willChangeValueForKey ------ start");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey ------ end");
}
-(void)didChangeValueForKey:(NSString *)key{
NSLog(@"willChangeValueForKey ------ start");
[super didChangeValueForKey:key];
NSLog(@"willChangeValueForKey ------ end");
}
@end
程序运行后,可以看到起打印结果为:
//willChangeValueForKey方法内部调用了一次getter
willChangeValueForKey ------ start
func is myName
willChangeValueForKey ------ end
//赋值 setter方法
func is setMyName:
//didChangeValueForKey方法内部调用了一次getter,并进行回调
didChangeValueForKey ------ start
func is myName
KVO 回调 : observeValueForKeyPath: ofObject: change: context:
didChangeValueForKey ------ end
可以看出,使用KVC对成员变量进行赋值的时候,是调用了以下两个方法的:
-(void)willChangeValueForKey:(NSString *)key
:在属性发生改变前调用- (void)didChangeValueForKey:(NSString *)key;
: 在属性发生改变后调用
如果在工程中,不希望别人通过使用KVC对属性进行赋值,来监听属性变化的话,可以使用KVC的方法+ (BOOL)accessInstanceVariablesDirectly
。这是因为在KVC中,查询不到属性的setter/getter方法的情况下,会查询该方法的返回值。如果该方法返回为true
,会直接去操作成员变量,对成员变量进行赋值或者取值;如果该方法返回为false
的话,就不会直接操作成员变量,而是抛出异常。所以如果重写了这个方法,并且针对key
的返回值是false,在willChangeValueForKey
中调用getter方法是,就会抛出异常,别人也就无法实现属性的监听。
KVO原理探究
最近一直在学习KVO的内容,所以也搜索了很多相关的文章,大牛们写的都很详细,也很清楚。本菜鸟是站在巨人的肩膀上看世界,大牛们高屋建瓴,我也是看了挺长时间,大牛们是摸着石头过河,我是摸着大牛们过河,也算是基本对于KVO的实现原理有了理解。
简单概括KVO的实现:
KVO的实现主要依靠Runtime技术,当一个对象的属性添加了观察者(observer)以后,系统会调用Runtime的相关方法,创建出一个中间类,这个类的类名以NSKVONotifying_
开头,并且继承于添加了观察者的属性所在的对象的类。并且在中间类中,重写了属性的setter
方法。在重写的setter
中,会调用父类的setter
,并且在调用父类setter
方法之前和之后,添加了通知该属性变化的方法。于此同时,重写了中间类的- (Class)class
方法,使得该方法的返回值仍然是原类。也会重写- (void)dealloc
,以完成相关的销毁工作。完成以上步骤后,修改原对象的isa
指针,使其指向这个中间类,这样操作以后,原来的对象就变成了中间类的实例对象。
验证以上说明,对添加KVO观察前后的对象进行打印,并打印其属性列表和方法列表,添加一下代码:
- (void)desObject{
//类名
NSString *classMethodName = NSStringFromClass([self.m2 class]);
NSString * objc_Runtime_Method_Name = NSStringFromClass(object_getClass(self.m2));
//成员变量列表
NSMutableArray *ivarStringList = [NSMutableArray array];
unsigned int invarsCount = 0;
Ivar *ivarList = class_copyIvarList(object_getClass(self.m2), &invarsCount);
for (int j=0; j<invarsCount; j++) {
Ivar i = ivarList[j];
const char *iName = ivar_getName(i);
[ivarStringList addObject:[NSString stringWithUTF8String:iName]];
}
//方法列表
NSMutableArray *methodStringList = [NSMutableArray array];
unsigned int count = 0;
Method *methodList = class_copyMethodList(object_getClass(self.m2), &count);
for (int i=0; i<count; i++) {
Method m = methodList[i];
SEL selector = method_getName(m);
[methodStringList addObject:NSStringFromSelector(selector)];
}
//打印
NSLog(@"classMethodName is %@ |--| objc_Runtime_Method_Name is %@",classMethodName,objc_Runtime_Method_Name);
NSLog(@"ivarList is %@",ivarStringList);
NSLog(@"methodStringList is %@",methodStringList);
NSLog(@"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
}
并在添加KVO前后添加该方法:
[self desObject];
[self.m2 addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:M2_KVONameContext];
[self desObject];
运行程序后,得到以下的信息:
classMethodName is SecondModel |--| objc_Runtime_Method_Name is SecondModel
ivarList is (
"_name"
)
methodStringList is (
name,
"setName:",
".cxx_destruct"
)
~~~~~~~~~~~~~~~~~^^添加KVO之前^^~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~vv添加KVO之后vv~~~~~~~~~~~~~~~~~~~~
classMethodName is SecondModel |--| objc_Runtime_Method_Name is NSKVONotifying_SecondModel
ivarList is (
)
methodStringList is (
"setName:",
class,
dealloc,
"_isKVOA"
)
又以上的打印结果可知:
使用
[self.m2 class]
打印的类名是SecondModel
,而使用object_getClass(self.m2)
打印的类名是NSKVONotifying_SecondModel
。中间类的方法列表中有
class
方法,说明中间类是重写了这个方法,并且重写后,该方法的返回值是原类。中间类的方法列表中有
setName:
方法,说明中间类也重写了这个方法。
KVO的自定义实现
研究了这两篇文章:KVO原理分析和runtime模拟实现KVO监听机制,也按照二位大神的方式,自己敲代码,把block形式的KVO进行了实现。在研究期间,对于KVO的实现原理理解的更清楚。有想要深刻了解的同学,可以看看这两篇文章,大有裨益。我这里对block形式的KVO总结了一个流程图,展示如下: