iOS-KVO(一) 基本操作
iOS-KVO(二) 使用注意點
iOS-KVO(三) 窺探底層實現(xiàn)
iOS-KVO(四) 自定義KVO+Block
KVO(key-value-observing)是實現(xiàn)鍵值觀察的方式,當某個屬性的值發(fā)生變化的時候,通知觀察者;
KVO本質(zhì)上其實是一個觀察者模式;
一般繼承自NSObject的對象都默認支持KVO;
(1)使用KVO的三個步驟
- 注冊O(shè)bserver;
- 接收屬性值的變化;
- 移除Observer;
先介紹以下每個步驟對應(yīng)的方法,以及對應(yīng)的參數(shù)的含義
注冊O(shè)bserver
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
對應(yīng)的參數(shù):
observer:觀察者,監(jiān)聽屬性變化的對象,該對象必須實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法;
keyPath:要觀察的屬性名稱;
options:調(diào)用接收方法的時機以及包含的內(nèi)容;
context:上下文,可以傳入任意類型的對象,將在消息回調(diào)的時,可以接收到這個對象,是KVO的一種傳值方式;
其中 options參數(shù):
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參數(shù)的中包含新的值(NSKeyValueChangeNewKey)
NSKeyValueObservingOptionOld:接收方法的change參數(shù)的中包含舊的值(NSKeyValueChangeOldKey)
NSKeyValueObservingOptionInitial:注冊的時候發(fā)一次通知,改變后也發(fā)送一次通知
NSKeyValueObservingOptionPrior:屬性改變之前發(fā)一次,改變之后再發(fā)一次,變化前的通知change參數(shù)包含notificationIsPrior = 1
接收方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
對應(yīng)的參數(shù):
keyPath:被觀察的屬性
object:被觀察的對象
change:在注冊時用options參數(shù)進行的配置,會包含不同的內(nèi)容
context:注冊的上下文
其中change的key(NSKeyValueChangeKey)有以下幾種
NSKeyValueChangeKindKey:對應(yīng)著四個修改的類型,其中后面三個是集合對象的操作方式
NSKeyValueChangeSetting = 1, 賦值
NSKeyValueChangeInsertion = 2, 插入
NSKeyValueChangeRemoval = 3, 移除
NSKeyValueChangeReplacement = 4, 替換NSKeyValueChangeNewKey:新值
NSKeyValueChangeOldKey:舊值
NSKeyValueChangeIndexesKey
NSKeyValueChangeNotificationIsPriorKey

移除觀察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
對應(yīng)的參數(shù):
observer:要移除的觀察者;
keyPath:要移除的屬性名稱;
注意:一定要在合適的機會移除觀察者,否則會引發(fā)泄露,我將會在下一篇中列舉出。
(2)觸發(fā)接收方法的幾種情況
- 直接調(diào)用setter方法,或者通過屬性的點語法間接調(diào)用;
- 使用KVC的setValue:forKey: 或者 setValue:forKeyPath:方法;
- 通過mutableArrayValueForKey:K方法獲取到數(shù)組代理對象,并使用代理對象進行操作;
注意:直接給成員賦值是不會觸發(fā)的
(3)使用案例
案例一:普通操作
#import <Foundation/Foundation.h>
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<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//點擊屏幕,設(shè)置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
點擊屏幕輸出結(jié)果:
2019-07-02 21:58:12.528122+0800 KVODemo[8046:234497] {
kind = 1;
new = 2;
old = "<null>";
}
案例二:觀察Person對象中的Info對象的name屬性
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Info : NSObject
@property (nonatomic, copy) NSString *name; //名字
@property (nonatomic, copy) NSString *address; //地址
@end
NS_ASSUME_NONNULL_END
#import <Foundation/Foundation.h>
#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<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//點擊屏幕,設(shè)置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
點擊屏幕輸出結(jié)果:
2019-07-02 22:40:45.128484+0800 KVODemo[8667:250652] {
kind = 1;
new = hui;
old = "<null>";
}
案例三:觀察Person對象中的Info對象多個屬性
代碼就不全部貼了,只貼重要代碼;
主要的是在被觀察者的對象中重寫
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key;
根據(jù)自己的需求,添加觀察的哪些屬性。如我想觀察info對象中的name和address的屬性。
那么代碼如下:
+ (NSSet<NSString *> *)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屬性變化時候,將會自動調(diào)用接收方法。
案例四:手動觸發(fā)
注冊后,KVO默認會自動通知觀察者。其實我們可以手動觸發(fā),在滿足某些條件我們在觸發(fā)調(diào)用接收方法。
如果你想取消自動通知,主動在被觀察者類中實現(xiàn)下面方法:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
返回值:
YES:主動觸發(fā);
NO:自動觸發(fā);
針對非自動通知的屬性,可以分別在變化之前和之后手動調(diào)用如下方法(will在前,did在后)來手動通知觀察者:
-(will/did)ChangeValueForKey:
-(will/did)ChangeValueForKey:withSetMutation:usingObjects:
-(will/did)Change:valuesAtIndexes:forKey:
實際上自動通知也是框架通過調(diào)用這些方法實現(xiàn)的。
直接用案例一的代碼,不全部列出來了。只列舉重要的部分。
觀察Person對象的name屬性,不讓其主動觸發(fā)。在Person類中加入
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ( [key isEqualToString:@"name"] ) {
return NO;
}
return YES;
}
觸摸屏幕時主動觸發(fā)接收方法。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//點擊屏幕,設(shè)置person的info的name屬性值
[self willChangeValueForKey:@"name"];
self.person.name = @"hui";
[self didChangeValueForKey:@"name"];
}
over!