參考官方文檔(developer.apple.com/library/arc… )html
先來看方法:- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;數組
其中observer和keyPath很容易理解,下面來詳細講解options和context安全
options 有以下四中配置bash
1.NSKeyValueObservingOptionNew 觀察者回調監聽中change字典中包含改變後的值app
2.NSKeyValueObservingOptionOld 觀察者回調監聽中change字典中包含改變前的值ui
3.NSKeyValueObservingOptionInitial 註冊後馬上觸發KVO通知atom
可是須要注意的是 NSKeyValueObservingOptions參數同時指定了NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial,首次觸發KVO change字典中並不包含old值spa
例子1:指針
#import "ViewController.h"
@interface Person : NSObject
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@end
@interface ViewController ()
@property (nonatomic, strong) Person * person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person = [[Person alloc] init];
//添加屬性觀察
[_person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:nil];
//觸發KVO
[_person setValue:@300 forKey:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%s",__func__);
NSLog(@"keyPath = %@",keyPath);
NSLog(@"change = %@",change);
}
- (void)dealloc
{
NSLog(@"%s",__func__);
//移除
[_person removeObserver:self forKeyPath:@"age"];
}
@end
複製代碼
打印結果以下:code
2019-12-26 15:26:11.683096+0800 KVC&KVO[3325:172468] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:26:11.683215+0800 KVC&KVO[3325:172468] keyPath = age
2019-12-26 15:26:11.683373+0800 KVC&KVO[3325:172468] change = {
kind = 1;
new = 300;
old = 0;
}
複製代碼
結果分析: 由於options同時指定了NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld,所以KVO回調接口字典change中同時包含new 和 old,若是隻指定了其中一個,那麼回調字典中就只有對應的一個
接着咱們將options參數改成: NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld| NSKeyValueObservingOptionInitial
2019-12-26 15:32:44.449559+0800 KVC&KVO[3351:175049] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:32:44.449682+0800 KVC&KVO[3351:175049] keyPath = age
2019-12-26 15:32:44.449822+0800 KVC&KVO[3351:175049] change = {
kind = 1;
new = 0;
}
2019-12-26 15:32:44.450094+0800 KVC&KVO[3351:175049] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:32:44.450185+0800 KVC&KVO[3351:175049] keyPath = age
2019-12-26 15:32:44.450317+0800 KVC&KVO[3351:175049] change = {
kind = 1;
new = 300;
old = 0;
}
複製代碼
結果分析:
1.因爲指定了NSKeyValueObservingOptionInitial,因此一旦添加觀察,就馬上觸發KVO(你能夠將 [_person setValue:@300 forKey:@"age"]註釋掉,它同樣會觸發KVO,也就是change字典中new =0的那一次).
2.options指定了 NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld| NSKeyValueObservingOptionInitial三個,可是首次馬上觸發的KVO回調字典並不包含old值
接着在Person類中添加:
//該方法用於修改是否容許自動KVO通知。默認容許返回YES,這裏咱們修改成NO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
NSLog(@"%s",__func__);
return NO;
}
複製代碼
再次運行:
2019-12-26 15:39:48.684258+0800 KVC&KVO[3389:178028] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:39:48.684373+0800 KVC&KVO[3389:178028] keyPath = age
2019-12-26 15:39:48.684523+0800 KVC&KVO[3389:178028] change = {
kind = 1;
new = 0;
}
2019-12-26 15:39:48.684614+0800 KVC&KVO[3389:178028] +[Person automaticallyNotifiesObserversForKey:]
複製代碼
結果分析:
接着將options指定 NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld| NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionPrior 同時Person容許自動KVO
運行打印:
2019-12-26 15:49:26.326606+0800 KVC&KVO[3435:181969] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:49:26.326725+0800 KVC&KVO[3435:181969] keyPath = age
2019-12-26 15:49:26.326886+0800 KVC&KVO[3435:181969] change = {
kind = 1;
new = 0;
}
2019-12-26 15:49:26.326981+0800 KVC&KVO[3435:181969] +[Person automaticallyNotifiesObserversForKey:]
2019-12-26 15:49:26.327225+0800 KVC&KVO[3435:181969] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:49:26.327311+0800 KVC&KVO[3435:181969] keyPath = age
2019-12-26 15:49:26.327438+0800 KVC&KVO[3435:181969] change = {
kind = 1;
notificationIsPrior = 1;
old = 0;
}
2019-12-26 15:49:26.327528+0800 KVC&KVO[3435:181969] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:49:26.327608+0800 KVC&KVO[3435:181969] keyPath = age
2019-12-26 15:49:26.327884+0800 KVC&KVO[3435:181969] change = {
kind = 1;
new = 300;
old = 0;
}
複製代碼
結果分析:
先來官方文檔怎麼說的.
使用方法addObserver:forKeyPath:options:context:添加觀察時,消息中的上下文指針能夠包含任意數據,而且這些數據將在相應的更改通知中傳遞迴觀察者. context能夠指定NULL並徹底依靠鍵路徑字符串來肯定更改通知的來源,可是這種方法可能會致使對象的父類因爲不一樣的緣由也觀察到相同的鍵路徑而致使問題. context能夠提供一種更安全,更可擴展的方法確保觀察者收到的通知是發給觀察者的,而不是父類對象的. 一個良好的上下文對象能夠是類中惟一命名的靜態變量的地址,在父類或子類中以相似方式選擇的上下文通常不會重複.能夠爲整個類選擇一個上下文,而後依靠通知消息中的關鍵路徑字符串來肯定更改的內容.此外,也能夠爲每一個觀察到的鍵路徑建立一個不一樣的上下文,從而徹底不須要進行字符串比較,從而能夠更有效地進行通知解析.
首先咱們不使用context,對單個對象簡單場景(例如上面的例子),貌似沒發現什麼不妥~ 可是一旦稍微有點複雜,不使用context那麼問題就很明顯了!
問題1: 同一個類的不一樣對象,須要添加觀察它們的age屬性,怎麼處理? 因爲要觀察的屬性都是age,也就是keyPath相同,可是Object對象不一樣,那麼你就不能依靠keyPath來區分通知來源了.此時你很容易想到使用Object來區分,這樣作也確實能夠. 例子以下:
#import "ViewController.h"
@interface Person : NSObject
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
NSLog(@"%s",__func__);
return [super automaticallyNotifiesObserversForKey:key];
}
@end
@interface ViewController ()
@property (nonatomic, strong) Person * person,*person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person = [[Person alloc] init];
_person2 = [[Person alloc] init];
//添加屬性觀察
[_person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:nil];
[_person2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:nil];
//觸發KVO
[_person setValue:@30 forKey:@"age"];
[_person2 setValue:@40 forKey:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if(object == _person){
NSLog(@"%s",__func__);
NSLog(@"keyPath = %@",keyPath);
NSLog(@"change = %@",change);
//接着讓_person去處理一些事
NSLog(@"已收到_person通知,讓_person對象去作些事");
}
else if(object == _person2){
NSLog(@"%s",__func__);
NSLog(@"keyPath = %@",keyPath);
NSLog(@"change = %@",change);
//接着讓_person2去處理一些事
NSLog(@"已收到_person2通知,讓_person2對象去作些事");
}else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc
{
NSLog(@"%s",__func__);
//移除
[_person removeObserver:self forKeyPath:@"age"];
[_person2 removeObserver:self forKeyPath:@"age"];
}
@end
複製代碼
2019-12-26 16:22:41.245756+0800 KVC&KVO[3546:194556] +[Person automaticallyNotifiesObserversForKey:]
2019-12-26 16:22:41.246065+0800 KVC&KVO[3546:194556] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 16:22:41.246162+0800 KVC&KVO[3546:194556] keyPath = age
2019-12-26 16:22:41.246317+0800 KVC&KVO[3546:194556] change = {
kind = 1;
new = 30;
old = 0;
}
2019-12-26 16:22:41.246411+0800 KVC&KVO[3546:194556] 已收到_person通知,讓_person對象去作些事
2019-12-26 16:22:41.246512+0800 KVC&KVO[3546:194556] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 16:22:41.246601+0800 KVC&KVO[3546:194556] keyPath = age
2019-12-26 16:22:41.246711+0800 KVC&KVO[3546:194556] change = {
kind = 1;
new = 40;
old = 0;
}
2019-12-26 16:22:41.246797+0800 KVC&KVO[3546:194556] 已收到_person2通知,讓_person2對象去作些事
複製代碼
經過object對象來區分對象通知來源(包括父類子類添加相同的屬性觀察,也均可以,可是不建議這樣作,由於可擴展性差,不夠安全),簡單場景確實行得通,由於這裏也不夠複雜.說到這裏,那咱們就來點複雜的
問題:2個不一樣類的對象,它們的屬性都不相同,暫且取其中兩個屬性來進行觀察,怎麼處理?那麼如今你要作的就是怎麼區分通知的來源,若是不使用context,你所想到的就是多重嵌套來判斷通知來源,有點相似下面的僞代碼:
if(object == 對象1){
//p1,p2 表明對象1屬性
if ([keyPath isEqualToString:@"p1"]) {
//....
}
else if ([keyPath isEqualToString:@"p2"]) {
//....
}
}else if(object == 對象2){
//pp1,pp2 表明對象2屬性
if ([keyPath isEqualToString:@"pp1"]) {
//....
}
else if ([keyPath isEqualToString:@"pp2"]) {
//....
}
}
複製代碼
看到上面的代碼你有沒有點抓狂的感受~~~ 這樣的代碼,容易出錯(一旦判斷出錯,錯誤的通知觀察者,就可能形成讓程序crash),並且擴展性不強(好比:我又更改需求了,如今變爲3個對象,3個不一樣屬性須要觀察,你如何處理?)
接下來咱們使用context上下文參數就能夠解決上述全部問題
#import "ViewController.h"
@interface Person : NSObject
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, assign) float height;
-(void)read;
@end
@implementation Person
-(void)read{
NSLog(@"人會閱讀書籍~~~");
}
@end
@interface Dog : NSObject
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, assign) float height;
-(void)run;
@end
@implementation Dog
-(void)run{
NSLog(@"狗狗會奔跑~~~");
}
@end
@interface ViewController ()
@property (nonatomic, strong) Person * person;
@property (nonatomic, strong) Dog * dog;
@end
@implementation ViewController
static void *PersonContext1 = &PersonContext1;
static void *PersonContext2 = &PersonContext2;
static void *DogContext1 = &DogContext1;
static void *DogContext2 = &DogContext2;
- (void)viewDidLoad {
[super viewDidLoad];
_person = [[Person alloc] init];
_dog = [[Dog alloc] init];
//添加屬性觀察
[_person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:PersonContext1];
[_person addObserver:self forKeyPath:@"height" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:PersonContext2];
[_dog addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:DogContext1];
[_dog addObserver:self forKeyPath:@"height" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:DogContext2];
//觸發KVO
_person.age = 20;
_person.height = 175;
_dog.age = 2;
_dog.height = 50;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonContext1) {
NSLog(@"object = %@",object);
NSLog(@"keyPath = %@",keyPath);
NSLog(@"change = %@",change);
[_person read];
}
else if (context == PersonContext2) {
NSLog(@"object = %@",object);
NSLog(@"keyPath = %@",keyPath);
NSLog(@"change = %@",change);
[_person read];
}
else if (context == DogContext1) {
NSLog(@"object = %@",object);
NSLog(@"keyPath = %@",keyPath);
NSLog(@"change = %@",change);
[_dog run];
}
else if (context == DogContext2) {
NSLog(@"object = %@",object);
NSLog(@"keyPath = %@",keyPath);
NSLog(@"change = %@",change);
[_dog run];
}
else{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc
{
NSLog(@"%s",__func__);
//移除
[_person removeObserver:self forKeyPath:@"age" context:PersonContext1];
[_person removeObserver:self forKeyPath:@"height" context:PersonContext2];
[_dog removeObserver:self forKeyPath:@"age" context:DogContext1];
[_dog removeObserver:self forKeyPath:@"height" context:DogContext2];
}
@end
複製代碼
打印結果以下:
2019-12-26 17:02:02.856237+0800 KVC&KVO[3692:209619] object = <Person: 0x6000000836a0>
2019-12-26 17:02:08.225155+0800 KVC&KVO[3692:209619] keyPath = age
2019-12-26 17:02:08.225185+0800 KVC&KVO[3692:209731] XPC connection interrupted
2019-12-26 17:02:09.256309+0800 KVC&KVO[3692:209619] change = {
kind = 1;
new = 20;
old = 0;
}
2019-12-26 17:02:14.048296+0800 KVC&KVO[3692:209619] 人會閱讀書籍~~~
2019-12-26 17:02:18.024598+0800 KVC&KVO[3692:209619] object = <Person: 0x6000000836a0>
2019-12-26 17:02:19.599899+0800 KVC&KVO[3692:209619] keyPath = height
2019-12-26 17:02:20.167343+0800 KVC&KVO[3692:209619] change = {
kind = 1;
new = 175;
old = 0;
}
2019-12-26 17:02:20.927309+0800 KVC&KVO[3692:209619] 人會閱讀書籍~~~
2019-12-26 17:02:35.256251+0800 KVC&KVO[3692:209619] object = <Dog: 0x600000083760>
2019-12-26 17:02:36.102929+0800 KVC&KVO[3692:209619] keyPath = age
2019-12-26 17:02:37.063650+0800 KVC&KVO[3692:209619] change = {
kind = 1;
new = 2;
old = 0;
}
2019-12-26 17:02:37.790867+0800 KVC&KVO[3692:209619] 狗狗會奔跑~~~
2019-12-26 17:02:43.469688+0800 KVC&KVO[3692:209619] object = <Dog: 0x600000083760>
2019-12-26 17:02:43.469830+0800 KVC&KVO[3692:209619] keyPath = height
2019-12-26 17:02:43.469978+0800 KVC&KVO[3692:209619] change = {
kind = 1;
new = 50;
old = 0;
}
2019-12-26 17:02:45.048043+0800 KVC&KVO[3692:209619] 狗狗會奔跑~~~
複製代碼
結果分析:
注意option參數指定NSKeyValueObservingOptionInitial觸發的KVO是沒法被automaticallyNotifiesObserversForKey:禁用的,因此咱們能作的就是:只能對除了option指定NSKeyValueObservingOptionInitial以外的觸發KVO方式進行禁用
禁用方式就是被觀察類重寫automaticallyNotifiesObserversForKey:並返回NO便可(這樣會禁用全部除了option參數指定NSKeyValueObservingOptionInitial之外的自動KVO)
單獨禁用某個key觸發的自動KVO能夠採用以下兩種方式: 單獨提供:automaticallyNotifiesObserversOfKey並返回NO便可
+ (BOOL)automaticallyNotifiesObserversOfAge{
return NO;
}
複製代碼
或者
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
//這裏age是你要禁用的key
if([key isEqualToString:@"age"]){
NSLog(@"對%@手動KVO",key);
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
複製代碼
1.常規setter方法
2.KVC
3.消息發送,調用setter方法(注意setter不存在狀況,須要動態處理)
4.手動KVO
1.手動KVO首先須要注意,option參數不能指定NSKeyValueObservingOptionInitial
2.禁用自動KVO(能夠參考以前如何禁用自動KVO)
3.賦值先後分別加入willChangeValueForKey:和didChangeValueForKey:方法便可(比較好的作法是在setter方法中)
[_person willChangeValueForKey:@"age"];
[_person setValue:@300 forKey:@"age"];
[_person didChangeValueForKey:@"age"];
複製代碼
5.依賴觸發KVO
有時候一個屬性的值依賴於另外一對象中的一個或多個屬性,若是這些屬性中任一屬性的值發生變動,被依賴的屬性值也應當爲其變動進行標記.最簡單的例子就是一我的的姓名fullName是由firstName和lastName組成,當firstName或者lastName發生改變的時候,fullName也會跟着改變.因此若是一個觀察者對fullName進行觀察,那麼當firstName或者lastName改變時,這個觀察者也應該被通知.
根據官方文檔描述有以下兩種解決方案:
方案1 :重寫keyPathsForValuesAffectingValueForKey:來指明fullName是依賴lastName和firstName的
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
複製代碼
方案2: 實現一個遵循命名方式爲keyPathsForValuesAffecting的類方法,是依賴於其餘值的屬性名
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
複製代碼
例子以下:
#import "ViewController.h"
@interface Person : NSObject
@property (nonatomic, copy) NSString* fullName,*lastName,*firstName;
@end
@implementation Person
- (NSString *)fullName {
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"%@ %@",_firstName, _lastName];
}
+ (NSSet *)keyPathsForValuesAffectingFullName {
NSLog(@"%s",__func__);
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSLog(@"%s",__func__);
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
@end
@interface ViewController ()
@property (nonatomic, strong) Person * person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person = [[Person alloc] init];
//添加屬性觀察
[_person addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
_person.firstName = @"Jay";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%s",__func__);
NSLog(@"keyPath = %@",keyPath);
NSLog(@"change = %@",change);
}
- (void)dealloc
{
NSLog(@"%s",__func__);
//移除
[_person removeObserver:self forKeyPath:@"fullName"];
}
@end
複製代碼
2019-12-27 17:27:35.457517+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingValueForKey:]
2019-12-27 17:27:35.457636+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingFullName]
2019-12-27 17:27:35.457752+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingValueForKey:]
2019-12-27 17:27:35.457858+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingValueForKey:]
2019-12-27 17:27:35.457946+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingValueForKey:]
2019-12-27 17:27:35.458045+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingValueForKey:]
2019-12-27 17:27:35.458427+0800 KVC&KVO[3492:220907] -[Person fullName]
2019-12-27 17:27:35.458532+0800 KVC&KVO[3492:220907] -[Person fullName]
2019-12-27 17:27:35.458627+0800 KVC&KVO[3492:220907] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-27 17:27:35.458709+0800 KVC&KVO[3492:220907] keyPath = fullName
2019-12-27 17:27:35.458854+0800 KVC&KVO[3492:220907] change = {
kind = 1;
new = "Jay (null)";
old = "(null) (null)";
}
複製代碼
結果分析:
上面都是一對一簡單場景,在一對多關係中(數組屬性),上述解決方案就無論用了.好比: 假若有一個部門,裏面有不少員工,每一個員工都有各自的薪水,如今要求統計這個部門全部員工的薪水總和. 這種狀況不能經過實現keyPathsForValuesAffectingTotalSalary方法並返回employees.salary
有兩種解決方法可供參考:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self updateTotalSalary];
}
else
// deal with other observations and/or invoke super...
}
- (void)updateTotalSalary {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
複製代碼