本文仅是记录自己在学习的过程中的理解:如有错误,还望各位大佬指正,THX.
KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。
1. KVO 的基本使用
相信大家在平时的开发中都使用过KVO,使用KVO分为3个步骤:
1.通过- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法注册观察者,观察者可以接收keyPath属性的变化事件。
参数部分:
--Observer参数:观察者对象
--keyPath参数:需要观察的属性,由于是字符串的形式,写错的话很容易导致崩溃,一般利用系统的反射机制NSStringFromSelector(@selector(keyPath));
--options参数:枚举类型
NSKeyValueObservingOptionNew 接收新值,默认为只接收新值
NSKeyValueObservingOptionOld 接收旧值
NSKeyValueObservingOptionInitial 在注册的时候立即接收一次回调,在改变是也会发生通知
NSKeyValueObservingOptionPrior 改变之前发一次,改变之后发一次
--context参数:传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式
**注意:在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致由于观察者的释放而带来的崩溃。
2.在观察者中实现-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法,当keyPath属性发生改变之后,KVO会回调这个方法来通知观察者属性的改变。
3.当观察者不需要监听的时候,可以调用- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;方法将KVO移除,需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致崩溃。一般在dealloc中调用。
KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。
2. KVO的触发模式
KVO在属性发生改变的时候默认是自动调用的,如果需要手动的控制这个调用时机,或者自己来实现KVO属性的调用,可以通过KVO提供的方法来调用。
在所要观察的对象.m文件中加入:
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
return YES;//默认,自动模式
return NO;//手动模式
}
同时在属性变化之前,调用:
- (void)willChangeValueForKey:(NSString *)key;
在属性变化之后,调用:
- (void)didChangeValueForKey:(NSString *)key;
其实无论属性的值是否发生改变,是否调用Setter方法,只要调用了willChangeValueForKey:和didChangeValueForKey:就会触发回调。
一般我们在开发的时候,需要用到KVO监听属性值得变化,一般不会将所有的值得监听都是手动的触发,同时我们也看到automaticallyNotifiesObserversForKey:传入了一个参数key, 就是为了让我们根据key来决定是否手动开启KVO.
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;//手动模式
}
return YES;//默认,自动模式
}
3. KVO属性依赖
如果在当前Person类中引入另外一个Dog类:
// Dog.h
@interface Dog : NSObject
@property (nonatomic,assign) NSInteger age;
@property (nonatomic,assign) NSInteger level;
@end
// Person.h
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,strong) Dog *dog;
@end
//Person.m
@implementation Person
-(instancetype)init
{
if (self = [super init]) {
_dog = [[Dog alloc] init];
}
return self;
}
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;//手动模式
}
return YES;//默认,自动模式
}
那么此时我们怎么通过Person来观察Dog类的age属性呢?
[_p addObserver:self forKeyPath:@"dog.age" options:NSKeyValueObservingOptionNew context:nil];
如果Dog类有多个属性;那么我们现在希望,只要Dog类中有属性的变化,就会通知到Person类,如果我们每一个属性都添加一遍观察者,是不是很麻烦,那么这里就需要用到属性依赖:我们在Person类的.m中添加一个方法:
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPath = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqual:@"dog"]) {
keyPath = [[NSSet alloc] initWithObjects:@"_dog.age",@"_dog.level", nil];
}
return keyPath;
}
同时在添加观察者时,不用对dog具体的属性添加:
[_p addObserver:self forKeyPath:@"dog" options:NSKeyValueObservingOptionNew context:nil];
4. KVO 的原理
KVO的其实就是观察属性的变化,也就是setter方法的变化,但是上面我们也提到过就是不需要调用setter方法同样可以触发KVO,那么KVO到底是不是观察setter方法呢?现在我们把代码恢复到最初的时候,此时只观察Person类的name属性,如果此时把name改成成员变量:
// Person.h
@interface Person : NSObject
{
@public
NSString *name;
}
//@property (nonatomic,copy) NSString *name;
@end
//调用改变name
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int a;
_p.name = [NSString stringWithFormat:@"%d",a++];
}
当改变name的值得时候,可以发现此时并不会有回调。
那么可以知道,其实KVO观察的还是属性的setter方法。
那么如何实现当调用Person类对象的setter方法的时候能够观察到改变呢?一般有两种方式实现:分类和子类继承。
那么我们可以试一下分类,创建一个Person的分类,并在分类里重写setName:方法,发现是可行的。但此时有一个隐患存在,因为此时我们已经在分类中实现了setName:方法,等于就是替换掉了Person类的setName:方法,此时Person类的setName:方法就不会被调用,而此时如果又需要重写Person类的setName:方法,那么就会出现影响。
在添加观察者的地方打个断点来看一下:KVO 底层实现:首先KVO需要创建一个子类(NSKVONotyfing_Person),这个子类是继承于被观察对象的,这个子类需要重写属性的setter方法,这个时候,外界在调用setter方法的时候,调用的是子类重写的setter方法。就是让外界的person对象的isa指针指向这个子类。
。此时Person类对象的isa指针指向的就是子类对象。
5. 自定义KVO
首先创建一个NSObject的分类:
// NSObject+KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (KVO)
-(void)KVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
// NSObject+KVO.m
#import "NSObject+KVO.h"
#import <objc/message.h>
@implementation NSObject (KVO)
-(void)KVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
//创建一个类
NSString *oldClassName = NSStringFromClass(self.class);
NSString *newClassName = [@"KVO_" stringByAppendingString:oldClassName];
Class MyClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
//注册类
objc_registerClassPair(MyClass); //MyClass继承于self.class 根据案例来看,此时MyClass继承于Person,那么此时MyClass这个子类是否具有父类Person的setName:方法呢? 没有,只不过我们在调用方法的时候,子类继承于父类,如果子类没有实现方法,回去父类中调用该方法,所以在潜意识上,我们人为子类具有父类的方法,所谓的重写子类的方法,其实就是给这个子类添加一个方法。
//重写setName方法
class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@");
//class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>)
//参数名称 参数
//Class 给那个类添加方法
//SEL 方法编号
//IMP 方法实现(指针)
//types 返回值类型
//修改isa指针
object_setClass(self, MyClass);
//将观察者保存到当前对象
objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_ASSIGN);
}
void setName(id self,SEL _cmd,NSString * newName){
NSLog(@"%@",newName);
//调用父类的setName:方法
Class class = [self class];//拿到当前类型
object_setClass(self, class_getSuperclass(class));//修改当前类型,变成父类
objc_msgSend(self, @selector(setName:),newName);
//拿到Observer,
id observer = objc_getAssociatedObject(self, @"observer");
if (observer) {
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",@{@"name":newName,@"kind":@1},nil);
}
//改回子类
object_setClass(self, class);
}
@end
这么写的KVO不会覆盖父类的set方法,也不会因为没有在dealloc中remove掉observer而崩溃掉。
6. 容器类的KVO
// Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,strong)NSMutableArray * array;
@end
// Person.m
-(NSMutableArray *)array
{
if (!_array) {
_array = [NSMutableArray array];
}
return _array;
}
// ViewController.m
[_p addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];
其实注册观察者的步骤与属性时一样的,只不过在修改array的时候有些变化,因为KVO监听的是set方法,而对array进行操作却不是set方法,这时候其实KVO提供了一个方法:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSMutableArray *tempArray = [_p mutableArrayValueForKey:@"array"];
[tempArray addObject:@"xxxx"];
//利用tempArray 去进行操作
}
通过断点来看一下tempArray的类型:
最后补充几个注意:
1.kvo的本质是什么?
利用runtimeAPI动态生成一个子类,并让instance对象的isa指向这个全新的子类,当修改instance对象的属性时,会调用willChangeValueForKey和didChangeValueForKey( 在父类原来的setter方法)并调用内部会触发监听器的监听方法(observerValueForKeyPath:)。
2.直接修改成员变量会触发KVO么?
不会触发KVO,添加KVO的Person实例,其实是NSKVONotyfing_Person类,再调用setter方法,不是调用Person的setter方法,而是NSKVONotyfing_Person的setter方法,因为修改成员变量不是setter方法赋值。
3.如果在项目中对Person类进行了监听,也创建了一个NSKVONotifying_Person类,那么会编译通过么?
编译通过,因为KVO是运行时刻创建的,并不在编译时刻,在编译时刻只有一个NSKVONotifying_Person,所以不报错,可以通过,但是此时KVO起不了作用。(KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class)