KVO是OC中观察者模式的一种实现,一个对象监测另一对象某属性是否发生变化,当被观察者某个属性发生改变时,会触发观察者的 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context 这个回调方法被执行
1、触发模式
首先添加观察者
[self.p addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
KVO在属性发生变化时是自动调用的,如果想手动调用或自己实现KVO属性的调用,则可通过在被观察者中实现automaticallyNotifiesObserversForKey,设置返回NO
//关闭自动观察者监听回调
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
//在修改属性的地方调用willChangeValueForKey和didChangeValueForKey
[self.p willChangeValueForKey:@"name"];
self.p.name = [NSString stringWithFormat:@"%@+",self.p.name];
[self.p didChangeValueForKey:@"name"];
则可以在观察者的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context中监听到变化
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"change=%@",change);
}
最后记得当观察者不需要监听时或者观察者在销毁前记得移除观察者
[self.p removeObserver:self forKeyPath:@"name"];
由上述可见:不自动调用观察回调的情况下,在存取数值的前后分别调用 2 个方法,也可以在监听回调中收到新值,
而在自动调用观察回调的情况下,只需要通过 .(点)语法修改,也可以在监听回调中监听,
2、KVO实现原理
我们在添加观察者前后打印出p的类
当被观察者(例如Person)为属性(name)添加观察者时,KVO在运行时动态创建了一个子类(NSKVONotifying_Person),并在这个子类中重写了(name)的setter方法,在其中观察属性变化情况并通知观察者的处理,而setter内部调用willChangeValueForKey和didChangeValueForKey方法并触发了observeValueForKey:ofObject:change:context: 回调方法
实现步骤:
1)创建了子类
2)重写了setter方法
3)改变了isa的指针
3、自定义KVO
我以Person为例(先写部分代码,后面会给全)
1、创建分类,分类中自定义添加观察者方法
2、在实现中创建、注册子类
objc_allocateClassPair、objc_registerClassPair
3、重写setter方法(因为子类中没有父类的方法,所以重写实质是添加了一个方法),例如 class_addMethod(kidClass, selector, (IMP)setterMethodCustom, "v@:@");
class_addMethod
4、修改isa指针
object_setClass
5、因为属性修改要告诉观察者,所以需要关联观察者属性绑定
objc_setAssociatedObject
6、在内部实现修改属性的方法void setterMethodCustom (id self,SEL cmd,id value)中,因为需要先调用父类的setter方法,所以需要
1)先保存当前类 Class kidClass = [self class];
2)修改isa指向父类object_setClass(self, class_getSuperclass(kidClass));
3)调用父类的setter方法((void ()(id,SEL,id)) objc_msgSend)(self, cmd, value);
在获取观察者objc_getAssociatedObject
通知观察者执行观察方法((void ()(id, SEL,id,id,id,id))objc_msgSend)(observer, @selector(observeValueForKeyPath:ofObject:change:context:),keyPath,self,@{@"old":oldValue,@"new":value},nil);
代码如下:
#import "Person.h"
@interface Person (KVOExt)
- (void)ymj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
#import "Person+KVOExt.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation Person (KVOExt)
- (void)ymj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
//自定义子类的类名
NSString * kidClassName = [NSString stringWithFormat:@"YMJKVONOtifying_%@",NSStringFromClass([self class])];
//创建子类
Class kidClass = objc_allocateClassPair(self.class, [kidClassName UTF8String], 0);
//注册子类
objc_registerClassPair(kidClass);
//setter方法名
NSString *firstString = [keyPath substringToIndex:1];
firstString = [firstString uppercaseString];
NSString *setterString = [keyPath substringFromIndex:1];
NSString * setterSelName = [NSString stringWithFormat:@"set%@%@:", firstString, setterString];
//重写setter方法,因为子类中没有父类的方法,所以重写实质是添加了一个方法
SEL selector = NSSelectorFromString(setterSelName);
class_addMethod(kidClass, selector, (IMP)setterMethodCustom, "v@:@");
//v@:@ => v表示返回值void,@表示对象,:表示SEl,因为oc方法中默认有两个参数(和消息发送机制有关),一个self,一个cmd,所以这里代表返回值为void,第一个参数self,第二个参数cmd,第三个参数要设置的新值value
//修改isa指针
object_setClass(self, kidClass);
//因为属性修改时候要告诉观察者,所以给子类添加属性关联
objc_setAssociatedObject(self, @"ymj_observer", observer, OBJC_ASSOCIATION_ASSIGN);
}
void setterMethodCustom (id self,SEL cmd,id value){
NSLog(@"value=%@",value);
//根据setter方法名获取属性key
NSString * setterSelName = NSStringFromSelector(cmd);
setterSelName = [setterSelName stringByReplacingOccurrencesOfString:@"set" withString:@""];
setterSelName = [setterSelName stringByReplacingOccurrencesOfString:@":" withString:@""];
NSString * keyPath = [NSString stringWithFormat:@"%@%@",[[setterSelName substringToIndex:1] lowercaseString],[setterSelName substringFromIndex:1]];
//获取老值
id oldValue = [self valueForKey:keyPath];
//更改isa指向父类,先执行父类的setter方法,保存属性的值
Class kidClass = [self class];
object_setClass(self, class_getSuperclass(kidClass));
((void (*)(id,SEL,id)) objc_msgSend)(self, cmd, value);
//获取观察者对象
id observer = objc_getAssociatedObject(self, @"ymj_observer");
if(observer){
//不能直接使用objc_msgSend的原型方法来匿名调用,否则会出现异常
((void (*)(id, SEL,id,id,id,id))objc_msgSend)(observer, @selector(observeValueForKeyPath:ofObject:change:context:),keyPath,self,@{@"old":oldValue,@"new":value},nil);
}
//更改isa指向为子类
object_setClass(self, kidClass);
}
4、KVO属性依赖
观察被观察者(例如:Person类)的属性(例如:Dog类)的属性(例如:age)时候,有两种方法处理
(1)监听的keypath直接写被观察者(p)的对象属性(dog).属性(age),例如"dog.age"
[self.p addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
(2)如果不仅仅监听age属性,还有dog中其他属性,推荐使用第二种方法,监听的keypath还是写被观察者(p)的属性(dog),而在被观察者(Person类)的内部实现keyPathsForValuesAffectingValueForKey方法,返回要观察的对象的属性的响应的属性,例如"_dog.age","_dog.level",则在修改dog的属性时,也能触发监听回调
//添加观察者
[self.p addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];
//在Person内部
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
if([key isEqualToString:@"dog"]){
return [NSSet setWithObjects:@"_dog.age",@"_dog.level", nil];
}
return [super keyPathsForValuesAffectingValueForKey:key];
}
5、KVO对容器类型的监听
因为KVO是通过在子类setter方法中触发监听回调,而单纯的使用addobject不会调用子类setter方法触发监听回调,测试发现有两种方法
(1)在修改前通过[对象 mutableArrayValueForKey:]会返回容器类的子类(NSKeyValueMutableArray,如下图),再使用addobject就能触发观察监听了
NSMutableArray * arr = [self.p mutableArrayValueForKey:@"dataArr"];
[arr addObject:self.p.name];
(2)考虑到setter内部是和willChangeValueForKey和didChangeValueForKey有关,于是测试手动在addobject前后调用willChangeValueForKey和didChangeValueForKey也能触发回调监听
[self.p willChangeValueForKey:@"dataArr"];
[self.p.dataArr addObject:self.p.name];
[self.p didChangeValueForKey:@"dataArr"];
不过这两种方法,在监听回调中,change信息会有所不同