KVO原理分析及使用进阶
1、概念
KVO(Key-Value-Oberver)观察者模式,是苹果提供的一套事件通知机制,允许对象监听另一个对象特定属性的改变,并在改变时接收事件,一般继承自NSObject的对象的都默认支持KVO
KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于:
- 1、 notification 比 KVO 多了发送通知的一步。
两者都是一对多,但是对象之间直接的交互,notification 明显得多,需要notificationCenter 来做为中间交互。而 KVO 如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了,只留一句“交由系统通知”,具体的可参照以上实现过程的剖析。- 2、 notification 的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。
2、基本使用
1、注册观察者
/*
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
observer:观察者 也就是被观察对象发生改变时通知的接收者
keyPath:被观察的属性名 比如我们这里是age属性
options:参数 这里一般选择NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld 也就是在回调方法里会受到被观察属性的旧值和新值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial枚举。
context:这个参数可以传入任意类型的对象,这个值会传递到接收消息回调的代码中,是KVO中的一种传值方式。
*/
self.penson = [[Person alloc]init];
[self.penson addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
注:forKeyPath: 参数可以改成NSStringFromSelector(@selector(age)),直接打字符,有可能会出现打错问题,NSStringFromSelector这个会检查提示没有这个属性
优化:
[self.penson addObserver:self forKeyPath:NSStringFromSelector(@selector(name)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
2、实现监听
//2.实现通知回调方法 当被观察对象的属性值发生变化时 就会回调这个方法 change字典中存放KVO属性相关的值,根据options时传入的枚举来返回。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@---%@----%@---%@",keyPath,object,change,context);
}
3、 一定要记得移除监听,要不然就反问野指针啦
//3.移除监听
[self.penson removeObserver:self forKeyPath:NSStringFromSelector(@selector(age)) ];
注意点
1: KVO的addObserver和removeObserver需要是成对的
KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。
苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。
2: 提个小问题: 添加观察者会导致循环引用吗?
当然不会,首先可验证,控制器关闭后,会执行delloc说明控制器被释放啦,没有导致循环引用,因为系统在实现KVO生成对象的子类是,使用的方法
objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_ASSIGN); 使用的是弱引用
使用OBJC_ASSOCIATION_RETAIN或者OBJC_ASSOCIATION_COPY就会导致循环引用啦,下面的原理探究会讲到这块, 听不懂啦吧,哈哈😺
3: 调用KVO属性对象时,不仅可以通过点语法和set语法进行调用,KVO兼容很多种调用方式:(关于KVC的实现原理接下来会讲到)
// 1.通过属性的点语法间接调用
self.penson.name = @"小王";
//2. 直接调用set方法
[self.penson setName:@"小王"];
// 3.使用KVC的setValue:forKeyPath:方法
[self.penson setValue:@"小王" forKeyPath:NSStringFromSelector(@selector(name))];
//4. 使用KVC的setValue:forKey:方法
[self.penson setValue:@"小王" forKey:NSStringFromSelector(@selector(name))];
// 5.通过mutableArrayValueForKey:方法获取到代理对象,并使用代理对象进行操作
4: 如果直接修改对象的成员变量是不会触发KVO的:
//PersonClass.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject{
@public;
NSString _name;//成员变量
}
//属性
@property (nonatomic, assign) NSString *name;
@end
直接修改成员变量,我们发现没有触发KVO
self.person -> _name = 234;
3 当对象里面又有更复杂的属性对象呢,怎么监控属性对象的的属性改变,如下面
#import <Foundation/Foundation.h>
@class AnimalClass;
@interface PersonClass : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) AnimalClass *animal;
@end
AnimalClass类中有一个name属性
@interface AnimalClass : NSObject
@property (nonatomic, copy) NSString *name;
@end
当我们对animal这个属性进行监听时,发现当对animal的属性值(name)修改时 kvo并不会监听到, 而当给person对象重新赋值一个新的animalClass对象时会被监听到
//会监听到改变 因为person1的animal属性是个指针 存储的是animal类型的一个地址值 当重新赋值一个alloc出来的新animalClass对象时 animal的地址值发生了改变 会调用person1的setAnimal方法
AnimalClass *ani2 = [[AnimalClass alloc]init];
ani2.name = @"cat";
self.person1.animal = ani2;
//不会被kvo监听到 因为修改animal的name属性 根本没有调用person1的setAnimal方法 只是调用了animal的setName方法
self.person1.animal.name = @"cat";
方式1:
而当我们对person.boy对象的age属性进行监听时 是可以监听到 self.person.boy.age = 12; 这种值改动的
self.person.boy = [[Boy alloc]init];
[self.person.boy addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
所以kvo能否监听到变化 要看这个被监听对象存储的是什么?实际上是否发生了改变?
方式2:
还是监控person对象,只是key改成@"boy.age", person对象属性boy,在加上boy对象属性age。
person.boy = [[Boy alloc]init];
[person addObserver:self forKeyPath: @"boy.age" options:NSKeyValueObservingOptionNew context:nil];
3、进阶用法
1 :(KVO触发模式)
当对象person有很多属性情况下,我并不需要一设置属性就自动触发监控,有需要触发有时候不需要触发,有什么方法不?
KVO触发模式,重写automaticallyNotifiesObserversForKey
返回YES,就是默认的自动模式,设置属性值就会触发KVO
返回NO,赋值不会触发KVO啦
/* Return YES if the key-value observing machinery should
automatically invoke -willChangeValueForKey:/-
didChangeValueForKey:, -willChange:valuesAtIndexes:forKey:/-
didChange:valuesAtIndexes:forKey:, or -
willChangeValueForKey:withSetMutation:usingObjects:/-
didChangeValueForKey:withSetMutation:usingObjects: whenever
instances of the class receive key-value coding messages for the key,
or mutating key-value coding-compliant methods for the key are
invoked. Return NO otherwise. Starting in Mac OS 10.5, the default
implementation of this method searches the receiving class for a
method whose name matches the pattern
+automaticallyNotifiesObserversOf<Key>, and returns the result of
invoking that method if it is found. So, any such method must return
BOOL too. If no such method is found YES is returned.
*/
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
#import "Person.h"
@implementation Person
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
@end
监控类中
[self.penson willChangeValueForKey:NSStringFromSelector(@selector(name))];
[self.penson.name = @"小碗";
[self.penson didChangeValueForKey:NSStringFromSelector(@selector(name))];
这个时候就需要在设置属性值之前调用willChangeValueForKey:
设置属性值之后调用didChangeValueForKey:
说明自动模式下直接设置属性值后,其实会帮我们调用这两个方法触发KVO,
也就是不改变属性值,直接调用这两句方法,也是会触发KVO的
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
/// 当改变属性值name时,需要手动去调用上述说的两个方法触发,其他属性就会不用调用也会自动触发
if ([key isEqualToString:@"name"]) {
return NO
}
return YES;
}
2 :(KVO关联属性)
// 设置boy的关联属性,这样想观察person对象的boy对象属性值变化,就不需要去使用
// 如 person addObserver: self forKeyPath: @"boy.age"
// 或者 person.boy addObserver: self forKeyPath: @"age" 这几种方式啦,麻烦而且boy属性值比较多还要一个个去监听
// person类实现如下方法,把boy类的属性关联过来就可以直接
// person addObserver: self forKeyPath: @"boy" 添加KVO
// 直接赋值 person.boy.age = 10, 就能出发KVO啦
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"boy"]) {
keyPaths = [[NSSet alloc]initWithObjects:@"_boy.age", @"_boy.height", nil];
}
return keyPaths;
}
self.person.boy = [[Boy alloc]init];
[self.person addObserver:self forKeyPath: NSStringFromSelector(@selector(boy)) options:NSKeyValueObservingOptionNew context:nil];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int a = 0;
self.person.boy.age = a++;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// 即能打印出boy改变的age
NSLog(@"%@", change);
}
4、高级玩法
自定义一个KVO
系统的KVO实现的原理是,当监控KVO时,会运用runtime生成对象的子类对象,然后重写属性的set方法,运用runtime直接调用- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
方法,自定义就是根据这个原理来的
如下
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (SHKVO)
/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
- (void)sh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)sh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)sh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+SHKVO.h"
#import <objc/message.h>
#import <objc/runtime.h>
@implementation NSObject (SHKVO)
/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
- (void)sh_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
//1. 创建一个类
NSString *oldClassName = NSStringFromClass(self.class);
NSString *newClassName = [@"SHKVO_" stringByAppendingString:oldClassName];
// 创建MyClass继承 self.class 的子类
Class MyClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
// 注册类
objc_registerClassPair(MyClass);
//2. 重写setNamea方法
/**
* class 给那个类添加方法
sel 方法编号
*/
class_addMethod(MyClass, @selector(setName:), (IMP)setName, @"v@:@");
//3. 修改isa指针!!
objc_setClass(self, MyClass);
//4. 将观察者保存到当前对象
objc_setAssocisatedObject(self, @"observer", observer, OBJC_ASSOCIATION_ASSIGN);
}
void setName(id self, SEL _cmd, NSString *newName) {
NSLog(@"来了!! %@", newName);
// 调用父类的setName方法,目的就是不印象父类的正常逻辑赋值
// 拿到当前类型
Class class = [self class];
objc_setClass(self, class_getSuperclass(class));
objc_msgSend(self, @selector(setName:), newName);
// 这个地方调用willchange,didChange,会不会触发KVO呢?
// 答,当然不会啦,这个时候已经走的是我们自定义的KVO,z系统的KVO你都没有添加监控呢
// 观察者
id observer = onjc_getAssociateObject(self, @"observer");
if (observer) {
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:contex:), @"name",
self, {@"new": newName, @"kind":@1}, nil);
}
// 改回子类
objc_setClass(self, class);
}
- (void)sh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)) {
}
- (void)sh_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
}
使用
person.boy = [[Boy alloc]init]
[person sh_addObserver:self forKeyPath: NSStringFromSelector(@selector(boy)) options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@" %@", change);
}
5、KVO 对容器类的监听
未完待续