探討KVO(OC)底層實現原理(二)

參考官方文檔(developer.apple.com/library/arc…html

KVO中API參數詳解

先來看方法:- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;web

  • observer表示觀察者(指定誰來監聽屬性改變,固然也能夠指定本身來監聽),NSObject類型,表示只要繼承了NSObject類型的對象(包括NSObject對象)均可以成爲觀察者
  • keyPath 表示要監聽的屬性
  • options 表示以何種配置來觀察屬性,既會影響通知中提供的更改字典的內容,又會影響生成通知的方式
  • context ,其類型爲void*,上下文對象,主要用於區分通知,提升安全

其中observer和keyPath很容易理解,下面來詳細講解options和context數組

options

options 有以下四中配置安全

  • 1.NSKeyValueObservingOptionNew 觀察者回調監聽中change字典中包含改變後的值bash

  • 2.NSKeyValueObservingOptionOld 觀察者回調監聽中change字典中包含改變前的值markdown

  • 3.NSKeyValueObservingOptionInitial 註冊後馬上觸發KVO通知架構

可是須要注意的是 NSKeyValueObservingOptions參數同時指定了NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial,首次觸發KVO change字典中並不包含old值app

  • 4.NSKeyValueObservingOptionPrior 值改變前是否通知(改變前通知一次,改變後再通知一次)

例子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
複製代碼

打印結果以下:oop

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:]
複製代碼

結果分析:

  • 1.首次馬上觸發的KVO沒有調用automaticallyNotifiesObserversForKey: 而值改變以後有調用automaticallyNotifiesObserversForKey:,不過此時返回NO,因此沒能觸發KVO
  • 2.options指定NSKeyValueObservingOptionInitial首次觸發的KVO通知,是沒法被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;
}
複製代碼

結果分析:

  • .因爲option添加了NSKeyValueObservingOptionPrior,所以在值修改前和修改後都會觸發KVO通知
注意事項:
  • 1.NSKeyValueObservingOptions指定NSKeyValueObservingOptionInitial,則一旦添加監聽馬上觸發KVO,沒法被automaticallyNotifiesObserversForKey:阻止,而且回調接口字典change中並不包含old值
  • 2.NSKeyValueObservingOptions指定NSKeyValueObservingOptionPrior,則屬性改變以前(具體在willChangeValueForKey會觸發)就會被通知一次,改變以後(具體在didChangeValueForKey會觸發)再通知一次

context

先來官方文檔怎麼說的.

image.png

使用方法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] 狗狗會奔跑~~~
複製代碼

結果分析:

  • 1.不用context很容易出錯,可能會致使錯誤的行爲,好比 讓一條狗去讀書~~~
  • 2 .使用context好處不言而喻,確實更加安全,擴展性強!

禁用自動KVO

  • 注意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];
}
複製代碼
  • automaticallyNotifiesObserversForKey:和automaticallyNotifiesObserversOfKey二者能夠共存,兩個方法同時存在時,優先走automaticallyNotifiesObserversForKey:方法 而後再走automaticallyNotifiesObserversOfKey,可是最後是否容許自動KVO由automaticallyNotifiesObserversForKey:決定

觸發KVO的幾種方式

  • 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)";
}
複製代碼

結果分析:

  • keyPathsForValuesAffectingValueForKey:和keyPathsForValuesAffectingFullName 能夠同時存在,二者也均會調用,可是最終結果會以keyPathsForValuesAffectingValueForKey:爲準

上面都是一對一簡單場景,在一對多關係中(數組屬性),上述解決方案就無論用了.好比: 假若有一個部門,裏面有不少員工,每一個員工都有各自的薪水,如今要求統計這個部門全部員工的薪水總和. 這種狀況不能經過實現keyPathsForValuesAffectingTotalSalary方法並返回employees.salary

有兩種解決方法可供參考:

  • 方法1:可使用鍵值觀察將父類(在此示例中爲Department)註冊爲全部子類(在此示例中爲Employees)的相關屬性的觀察者。必須在把child添加或刪除到parent時也把parent做爲child的觀察者添加或刪除。在observeValueForKeyPath:ofObject:change:context:方法中,將更新依賴值以響應更改,如如下代碼片斷所示:
- (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;
}
複製代碼
  • 方法2:若是使用的是Core Data,則能夠在應用程序的通知中心將父級註冊爲其託管對象上下文的觀察者。 父類應以相似於觀察鍵值的方式響應子類發送的相關變動通知

看看例子:

#import "ViewController.h"

@class Department;

@interface Employee : NSObject

@property (nonatomic, assign) float salary;

@end

@implementation Employee

@end


@interface Department : NSObject


@property (nonatomic, strong) NSArray<Employee*> * employees;

@property (nonatomic, strong) NSNumber *totalSalary;

@end


@implementation Department


@synthesize totalSalary = _totalSalary;

static void *totalSalaryContext = &totalSalaryContext;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _employees = @[];
        [self addObserver:self forKeyPath:@"employees" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:totalSalaryContext];
        
    }
    return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"%s",__func__);
    NSLog(@"keyPath = %@",keyPath);
    NSLog(@"change = %@",change);
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else{
        
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
 
- (void)updateTotalSalary {
    
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (_totalSalary != newTotalSalary) {
        _totalSalary = newTotalSalary;
    }
}
 
- (NSNumber *)totalSalary {
    
    return _totalSalary;
}
- (void)dealloc
{
    NSLog(@"%s",__func__);
    [self removeObserver:self forKeyPath:@"employees" context:totalSalaryContext];
}
@end


@interface ViewController ()

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
     Department *department = [[Department alloc] init];
    NSMutableArray* salaries = [department mutableArrayValueForKeyPath:@"employees"];
    for (int i =0 ; i< 5; i++) {
        Employee* emp = [[Employee alloc] init];
        emp.salary = 5000+ i*100;
        [salaries addObject:emp];
        
    }

    [salaries removeAllObjects];
    
}
@end
複製代碼

KVO的使用注意事項

  • 1.移除一個還沒有註冊的觀察者將致使NSRangeException.能夠對removeObserver:forKeyPath:context:和 addObserver:forKeyPath:options:context:的調用放在在try / catch塊內處理潛在的異常
  • 2.觀察者被釋放後,觀察者不會自動刪除本身。被觀察對象仍然會繼續發送通知,而忽略了觀察者的狀態,則會形成內存訪問異常。須要確保觀察者在從內存中消失以前將本身刪除!
  • 3.確保成對和有序地添加和刪除觀察,而且確保觀察者在註冊以前先未註冊,移除以前未被移除.一種典型的模式是在觀察者初始化期間(例如,在init或viewDidLoad中)註冊爲觀察者,並在釋放過程當中(一般在dealloc中)註銷
  • 4.被觀察屬性是集合屬性(例如:NSArray)時,add和move操做不會觸發KVO(例如被觀察是NSArray屬性,能夠結合mutableArrayValueForKeyPath以後來進行add 或者remove來觸發KVO)

KVO的使用場景

    1. iOS MVC架構模式 (好比:M和C之間通訊,監聽模型屬性實時更新UI)
    1. macOS X Cocoa Bindings 技術
    1. 定製UI 好比:監聽 content offset實現上下拉刷新控件(MJRefresh框架) ,監聽content size實現webview混合排版等
相關文章
相關標籤/搜索