- 概念
- 基本使用
- 触发模式
- 属性依赖
- 容器类的使用
- 自定义KVO
概念
KVO全称Key-Value Observing,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
基本使用
使用KVO分为三个步骤:
1.通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
2.在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
3.当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。
注册方法
/**
注册KVO监听
@param observer 观察者对象
@param keyPath 需要观察的属性,由于是字符串形式,容易crash,一般利用系统的反射机制 NSStringFromSeletor(seletor(keypath))
@param options 监听枚举类型
OptionNew 接收新值,默认为只接收新值
OptionOld 接收旧值
OptionInitial在注册时立即接收一次回调,在改变时也会发送通知
OptionPrior 改变之前发一次,改变之后发一次
@param context 传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式,主要用于多个监听器对象监听相同keypath时进行区分
*/
-(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
tips:在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的carsh.
KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。
苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。
监听方法
观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash。
/**
监听回调方法
@param keyPath 监听的属性路径
@param object 被观察对象
@param change 监听内容的变化,是个字典
@param context 一个用来传值的对象,由注册方法添加
*/
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
触发模式
自动触发
控制当前对象的自动调用过程
/**
调用模式,是否响应
@param key KVO观察属性
@return YES 正常发送通知; NO 不发送通知
*/
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
手动触发
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
属性依赖
A类的属性是一个拥有多个属性的对象B,要观察该对象,而不想写多个addObserver方法,
可以在A类的.m文件中重写
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
//dog属性的相关属性发生变化, 避免因为监听dog的多个属性而写多份KVO监听代码
if ([key isEqualToString:@"dog"]) {
keyPaths = [NSSet setWithObjects:@"_dog.age",@"_dog.color",nil];
}
return keyPaths;
}
容器类的使用
//监听容器类
[self.person addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];
//该方法无法触发
[self.person.array addObject:@"元素"];
//使用以下方法可以触发,
NSMutableArray *tempArray = [self.person mutableArrayValueForKey:@"array"];
[tempArray addObject:@"元素"];
自定义KVO
KVO是通过isa-swizzling技术实现的
1.创建一个子类 NSKVONotifying_Person,继承于被观察的类
2.重写setter方法
3.外界改变isa指针
object_getClassName(p)
开始是原类: isa指向Person
p addObserver:forKeyPath:options:context:
注册监听之后:isa指向 NSKVONotifying_Person
根据这个思路,我自己写了一个自定义KVO,仅供技术交流,不能在项目中直接使用
#import "MyKVOPerson+KVO.h"
#import <objc/message.h>
@implementation MyKVOPerson (KVO)
- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
//创建子类
//创建一个新的类 模仿系统类 NSKVONotifying_MyKVOPerson
NSString *oldClassName = NSStringFromClass(self.class);
NSString *newClassName = [@"MYKVONotifying_" stringByAppendingString:oldClassName];
//创建类 并且注册类
Class myClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
objc_registerClassPair(myClass);
//重写setName方法 实际是添加一个和父类同名的方法
class_addMethod(myClass, @selector(setName:), (IMP)setName, "v@:@");
//外部改变isa指针指向
object_setClass(self, myClass);
//属性绑定 将观察者保存到当前对象 OBJC_ASSOCIATION_ASSIGN weak 防止循环引用
objc_setAssociatedObject(self, @"Observer", observer, OBJC_ASSOCIATION_ASSIGN);
}
void setName(id self,SEL _cmd,NSString *newName) {
NSLog(@"修改成功");
//调用父类的setName方法,改变name的值
Class class = [self class];
object_setClass(self, class_getSuperclass(class));
//
((void(*)(id,SEL,NSString *))objc_msgSend)(self,@selector(setName:),newName);
//拿到观察者
id observer = objc_getAssociatedObject(self, @"Observer");
if (observer) {
//发消息
((void (*)(id, SEL,NSString*,id, NSDictionary<NSKeyValueChangeKey,id> *,void *))objc_msgSend)(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"name":newName,@"kind":@1},nil);
}
//修改为self,改回子类
object_setClass(self, class);
}
详细Dmeo请移步GitHub👉KVODemo
网友评论