iOS-KVO(一) 基本操作
iOS-KVO(二) 使用注意点
iOS-KVO(三) 窥探底层实现
iOS-KVO(四) 自定义KVO+Block
KVO(key-value-observing)是实现键值观察的方式,当某个属性的值发生变化的时候,通知观察者;
KVO本质上其实是一个观察者模式;
一般继承自NSObject的对象都默认支持KVO;
(1)使用KVO的三个步骤
- 注册Observer;
- 接收属性值的变化;
- 移除Observer;
先介绍以下每个步骤对应的方法,以及对应的参数的含义
注册Observer
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
对应的参数:
observer:观察者,监听属性变化的对象,该对象必须实现observeValueForKeyPath:ofObject:change:context:方法;
keyPath:要观察的属性名称;
options:调用接收方法的时机以及包含的内容;
context:上下文,可以传入任意类型的对象,将在消息回调的时,可以接收到这个对象,是KVO的一种传值方式;
其中 options参数:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
NSKeyValueObservingOptionNew:接收方法的change参数的中包含新的值(NSKeyValueChangeNewKey)
NSKeyValueObservingOptionOld:接收方法的change参数的中包含旧的值(NSKeyValueChangeOldKey)
NSKeyValueObservingOptionInitial:注册的时候发一次通知,改变后也发送一次通知
NSKeyValueObservingOptionPrior:属性改变之前发一次,改变之后再发一次,变化前的通知change参数包含notificationIsPrior = 1
接收方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
对应的参数:
keyPath:被观察的属性
object:被观察的对象
change:在注册时用options参数进行的配置,会包含不同的内容
context:注册的上下文
其中change的key(NSKeyValueChangeKey)有以下几种
NSKeyValueChangeKindKey:对应着四个修改的类型,其中后面三个是集合对象的操作方式
NSKeyValueChangeSetting = 1, 赋值
NSKeyValueChangeInsertion = 2, 插入
NSKeyValueChangeRemoval = 3, 移除
NSKeyValueChangeReplacement = 4, 替换NSKeyValueChangeNewKey:新值
NSKeyValueChangeOldKey:旧值
NSKeyValueChangeIndexesKey
NSKeyValueChangeNotificationIsPriorKey
移除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
对应的参数:
observer:要移除的观察者;
keyPath:要移除的属性名称;
注意:一定要在合适的机会移除观察者,否则会引发泄露,我将会在下一篇中列举出。
(2)触发接收方法的几种情况
- 直接调用setter方法,或者通过属性的点语法间接调用;
- 使用KVC的setValue:forKey: 或者 setValue:forKeyPath:方法;
- 通过mutableArrayValueForKey:K方法获取到数组代理对象,并使用代理对象进行操作;
注意:直接给成员赋值是不会触发的
(3)使用案例
案例一:普通操作
#import
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSNumber *PID; //人ID
@property (nonatomic, copy) NSString *name; //名字
@property (nonatomic, copy) NSString *address; //地址
@end
NS_ASSUME_NONNULL_END
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
self.person = p;
[p addObserver:self forKeyPath:@"PID" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//点击屏幕,设置person的PID属性值
static NSInteger count = 1;
count++;
self.person.PID = @(count);
}
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"PID"];
self.person = nil;
NSLog(@"%@ dealloc", [self class]);
}
@end
点击屏幕输出结果:
2019-07-02 21:58:12.528122+0800 KVODemo[8046:234497] {
kind = 1;
new = 2;
old = "";
}
案例二:观察Person对象中的Info对象的name属性
#import
NS_ASSUME_NONNULL_BEGIN
@interface Info : NSObject
@property (nonatomic, copy) NSString *name; //名字
@property (nonatomic, copy) NSString *address; //地址
@end
NS_ASSUME_NONNULL_END
#import
#import "Info.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSNumber *PID; //人ID
@property (nonatomic, strong) Info *info; //个人信息
@end
NS_ASSUME_NONNULL_END
#import "Person.h"
@implementation Person
- (instancetype)init
{
self = [super init];
if ( self ) {
_info = [Info new];
}
return self;
}
@end
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
self.person = p;
//观察person的info的name属性
[self.person addObserver:self forKeyPath:@"info.name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//点击屏幕,设置person的info的name属性值
self.person.info.name = @"hui";
}
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"info.name"];
self.person = nil;
NSLog(@"%@ dealloc", [self class]);
}
@end
点击屏幕输出结果:
2019-07-02 22:40:45.128484+0800 KVODemo[8667:250652] {
kind = 1;
new = hui;
old = "";
}
案例三:观察Person对象中的Info对象多个属性
代码就不全部贴了,只贴重要代码;
主要的是在被观察者的对象中重写
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key;
根据自己的需求,添加观察的哪些属性。如我想观察info对象中的name和address的属性。
那么代码如下:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ( [key isEqualToString:@"info"] ) {
keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"_info.name", @"_info.address"]];
}
return keyPaths;
}
观察的keyPaths为info。
[self.person addObserver:self forKeyPath:@"info" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
当info对象的name或者address属性变化时候,将会自动调用接收方法。
案例四:手动触发
注册后,KVO默认会自动通知观察者。其实我们可以手动触发,在满足某些条件我们在触发调用接收方法。
如果你想取消自动通知,主动在被观察者类中实现下面方法:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
返回值:
YES:主动触发;
NO:自动触发;
针对非自动通知的属性,可以分别在变化之前和之后手动调用如下方法(will在前,did在后)来手动通知观察者:
-(will/did)ChangeValueForKey:
-(will/did)ChangeValueForKey:withSetMutation:usingObjects:
-(will/did)Change:valuesAtIndexes:forKey:
实际上自动通知也是框架通过调用这些方法实现的。
直接用案例一的代码,不全部列出来了。只列举重要的部分。
观察Person对象的name属性,不让其主动触发。在Person类中加入
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ( [key isEqualToString:@"name"] ) {
return NO;
}
return YES;
}
触摸屏幕时主动触发接收方法。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//点击屏幕,设置person的info的name属性值
[self willChangeValueForKey:@"name"];
self.person.name = @"hui";
[self didChangeValueForKey:@"name"];
}
over!