KVO使用及原理簡述

介紹

工程中我們常常需要得到成員變量屬性的值的改變, 在iOS開發(fā)中:

  • 成員變量屬性指對象的參數(shù), 如: 一個人的名字: person.name

  • 成員變量或?qū)傩?/code>的成員變量或?qū)傩?/code>指對象的參數(shù)的參數(shù), 如: 一個人的孩子的名字: person.child.name

    如我們需要實時得到某個用戶的信用情況, 針對不同的信用等級, 我們有不同的操作. 我們定個屬性: user.credit:

  • 當(dāng)user.credit == great, 圣誕節(jié)到了, 我們給他送個禮物

  • 當(dāng)user.credit == good, 我們提升這個用戶的信用額度

  • 當(dāng)user.credit == ok, 我們給他打個標(biāo)簽: 優(yōu)質(zhì)用戶

  • 當(dāng)user.credit == bad, 我們關(guān)閉他的借款權(quán)限

在上述情況下, 我們可以使用Cocoa提供給我們的KVO(Key-value observing)來實現(xiàn):

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

KVO也體現(xiàn)了在iOS開發(fā)中常使用的一種設(shè)計模式 - 觀察者設(shè)計模式.

KVO 的使用

步驟

  1. 添加監(jiān)聽: addObserver: forKeyPath: options: context:
  2. 實現(xiàn)監(jiān)聽方法: observeValueForKeyPath: ofObject: change: context:
  3. 移除監(jiān)聽: removeObserver: forKeyPath:

示例

  1. ViewController創(chuàng)建一個屬性
@property (nonatomic, copy) NSString *name;
  1. 添加key-value-observer
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
  1. 實現(xiàn)監(jiān)聽值(此處為name)變化時的監(jiān)聽方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"name = %@", self.name);
    }
}
  1. ViewControllerdealloc中移除
- (void)dealloc{
    [self removeObserver:self forKeyPath:@"name"];
}
  • 注: 移除observer視實際情況而定, 也可以在viewDidDisappear:或者處理完監(jiān)聽, 在observeValueForKeyPath: ofObject: change: context:最后.

測試

  1. viewController中添加一個title更改name 的按鈕, 為其添加一個事件, 用來修改name, 如下:
static NSInteger idx;
///修改name
- (IBAction)modifyNameAction:(id)sender {
    
    NSArray *nameArr = @[@"張三", @"李四", @"王五", @"趙六", @"Jim七", @"David八", @"Kevin九", @"Danny十"];
    self.name = nameArr[idx];
    idx++;
    if (idx > 9) {
        idx = 0;
    }
    
}
  • 注: 此處為了使代碼緊湊, 未優(yōu)化nameArr
  1. 點擊按鈕, 更改name屬性, 可以看到KVO的監(jiān)聽方法被觸發(fā):
    KVO觸發(fā).gif
以上即為KVO的基本使用, 也是系統(tǒng)的自動調(diào)用. KVO自動調(diào)用的原理為:
  1. 系統(tǒng)會重寫被監(jiān)聽屬性的setter方法, 如上述的setName:, 所以, 必須監(jiān)聽屬性, 有setter方法
  2. 系統(tǒng)會依次調(diào)用:
  • 1)- willChangeValueForKey:
  • 2)setter方法
  • 3)- didChangeValueForKey:
  • 4)通知觀察者.
    這也解釋了NSKeyValueObservingOptionOld(舊值)NSKeyValueObservingOptionNew(新值)的來源.

驗證:

重寫setter, willChangeValueForKey:, didChangeValueForKey:

- (void)setName:(NSString *)name{
    NSLog(@"22---setter");
    _name = name;
}

- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"11---will key = %@", key);
}

- (void)didChangeValueForKey:(NSString *)key{
    [super didChangeValueForKey:key];
    NSLog(@"33--- did key = %@", key);
    
}

觀察打印如下:


方法調(diào)用順序.gif
  • 有自動調(diào)用, 就有手動調(diào)用, 手動調(diào)用我們將在后面講述.

監(jiān)聽一個屬性, 實現(xiàn)監(jiān)聽多個屬性

我們使用間接屬性來舉例

  1. 定義一個Child類, 它有4個屬性: birthday, year, month, day:
///生日
@property (nonatomic, copy) NSString *birthday;
///生日的年
@property (nonatomic, assign) NSInteger year;
///生日的月
@property (nonatomic, assign) NSInteger month;
///生日的日
@property (nonatomic, assign) NSInteger day;
  1. Child.m 中, 初始化上述屬性:
- (instancetype)init{
    if (self = [super init]) {
        self.birthday = @"2000-01-01";
        self.year = 2000;
        self.month = 1;
        self.day = 1;
    }
    return self;
}
  1. 定義一個Worker類, 它有一個Child屬性:
@property (nonatomic, strong) Child *child;
  1. viewController 類中添加一個worker屬性:
@property (nonatomic, strong) Worker *worker;
  1. 監(jiān)聽worker 的child 中birthday 的改變:
[self.worker addObserver:self forKeyPath:@"child.birthday" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
  1. 添加更改間接屬性的事件:
//更改間接屬性值事件
- (IBAction)modifyObjectAction:(id)sender {
    self.worker.child.birthday = @"2001-12-31";
}

這樣在監(jiān)聽方法中, 我們便能得到worker.child.birthday 更改前后的值:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
   
    if ([keyPath isEqualToString:@"child.birthday"]) {
        NSLog(@“change info = %@", change);
    }
}
監(jiān)聽間接屬性`child.birthday`.gif
  • 上例中, 對于Child來說, 其屬性birthday是由year, month, day影響的, 即當(dāng)year, month, day其一改變時, 關(guān)心birthday的外界也需要收到監(jiān)聽. 這種情況下, 當(dāng)Childyear, month 或 day改變時, 應(yīng)當(dāng)告訴birthday的監(jiān)聽者.
  • 這里就需要實現(xiàn) KVO 的這個方法:
    + (NSSet<NSString *> *)keyPathsForValuesAffectingKey
  • 在我們重寫這個方法, 系統(tǒng)自動補全提示時, 會將Key替換成我們的屬性名稱. 如此處重寫的方法為:
///當(dāng)kvo當(dāng)前對象的birthday屬性時,如果year,month,day的值發(fā)生變化,都會觸發(fā)這個KVO
+ (NSSet<NSString *> *)keyPathsForValuesAffectingBirthday{
    return [NSSet setWithObjects:NSStringFromSelector(@selector(year)),
            NSStringFromSelector(@selector(month)),
            NSStringFromSelector(@selector(day)),
            nil];
}

這樣, 只要KVO監(jiān)聽了birthday , 當(dāng)year, month, day 改變時, 也會觸發(fā)監(jiān)聽方法.

  • 注: 這種操作, 我們在change中得到的還是birthday的值.

監(jiān)聽數(shù)組

實際上, 能使用KVO來監(jiān)聽的屬性, 必須符合Key-Value Coding, 而數(shù)組并不符合.
所以, 直接監(jiān)聽數(shù)組屬性, 用數(shù)組默認(rèn)的API來操作數(shù)組時, 是不會觸發(fā)監(jiān)聽方法的.
實現(xiàn):

  1. 被監(jiān)聽的對象需要實現(xiàn)下面方法
  2. 且操作數(shù)組屬性時, 也要使用下面對應(yīng)的方法:
- objectInMyArrayAtIndex:

- insertObject:inMyArrayAtIndex:

- removeObjectFromMyArrayAtIndex:

- replaceObjectInMyArrayAtIndex:withObject:

同KVO的其它方法一樣, 重寫這些方法時, 系統(tǒng)也會有補全提示, 而上述中的MyArray會替換成實際的屬性名稱.

  1. 依然在上述例子中, 我們?yōu)?code>worker添加一個cities屬性:
@property (nonatomic, strong) NSMutableArray *cities;
  1. Worker.m中初始化:
- (instancetype)init{
    if (self = [super init]) {
        self.cities = [NSMutableArray array];
    }
    return self;
}
  1. 實現(xiàn)KVO數(shù)組相關(guān)的方法:
- (id)objectInCitiesAtIndex:(NSUInteger)index{
    return [self.cities objectAtIndex:index];
}

- (void)insertObject:(NSString *)object inCitiesAtIndex:(NSUInteger)index{
    [self.cities insertObject:object atIndex:index];
}

- (void)removeObjectFromCitiesAtIndex:(NSUInteger)index{
    [self.cities removeObjectAtIndex:index];
}

- (void)replaceObjectInCitiesAtIndex:(NSUInteger)index withObject:(id)object{
    [self.cities replaceObjectAtIndex:index withObject:object];
}

- (void)addCitiesObject:(NSString *)object{
    [self.cities addObject:object];
}
  1. 并在Worker.h文件中公開上述方法:
- (id)objectInCitiesAtIndex:(NSUInteger)index;

- (void)insertObject:(NSString *)object inCitiesAtIndex:(NSUInteger)index;

- (void)removeObjectFromCitiesAtIndex:(NSUInteger)index;

- (void)replaceObjectInCitiesAtIndex:(NSUInteger)index withObject:(id)object;

- (void)addCitiesObject:(NSString *)object;
  1. viewController中監(jiān)聽:
    [self.worker addObserver:self forKeyPath:NSStringFromSelector(@selector(cities)) options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
  1. 依次執(zhí)行下面方法:
    [self.worker insertObject:@"nanjing" inCitiesAtIndex:0];
    [self.worker insertObject:@"suzhou" inCitiesAtIndex:1];
    [self.worker replaceObjectInCitiesAtIndex:1 withObject:@"wuxi"];
    [self.worker removeObjectFromCitiesAtIndex:self.worker.cities.count-1];

過濾掉無用信息后, 對應(yīng)打印結(jié)果如下:

//1. [self.worker insertObject:@"nanjing" inCitiesAtIndex:0];
 change info = {
    kind = 2;
    new =     (
        nanjing
    );
}
 //2. [self.worker insertObject:@"suzhou" inCitiesAtIndex:1];
change info = {
    kind = 2;
    new =     (
        suzhou
    );
}
 //3. [self.worker replaceObjectInCitiesAtIndex:1 withObject:@"wuxi"];

change info = {
    kind = 4;
    new =     (
        wuxi
    );
    old =     (
        suzhou
    );
}
  //4. [self.worker removeObjectFromCitiesAtIndex:self.worker.cities.count-1];

change info = {
    kind = 3;
    old =     (
        wuxi
    );
}

因為字典change中存儲的是變化的數(shù)組元素的值, 而不是整個數(shù)組的值, 所以對應(yīng)步驟解析如下:

  • 1.添加.所以只有新值,沒有舊值
  • 2.同上
  • 3.替換.新值替換舊值, 所以既有舊值,也有新值
  • 4.刪除.只是刪除舊值, 沒有新值加入,所以只有舊值
    • 注:添加元素時,只能insertObject:AtIndex, 沒有直接addObject:

關(guān)閉系統(tǒng)自動調(diào)用KVO, 改為手動調(diào)用

在很多情況下, 我們都應(yīng)該關(guān)閉自動調(diào)用, 改為手動調(diào)用. 因為每次調(diào)用setter, 都會調(diào)用監(jiān)聽方法, 即使舊值與新值相同.

如我們要關(guān)閉屬性name的自動調(diào)用
  1. 重寫觸發(fā)手動或自動調(diào)用的類方法, 并返回NO. 如
+ (BOOL)automaticallyNotifiesObserversOfName{
    return NO;
}

或者

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    //name之外的屬性,還是由系統(tǒng)自動調(diào)用
    return YES;
}

是的, 正如你所料, 系統(tǒng)是默認(rèn)返回YES

  1. name改變, 需要觸發(fā)監(jiān)聽方法observeValueForKeyPath: ofObject: change: context:時, 手動調(diào)用-willChangeValueForKey:- didChangeValueForKey:
實現(xiàn)
  1. 把我們的名字?jǐn)?shù)組的李四變成張三, 這樣我們就有兩個張三了:

    兩個`張三.png

  2. 重寫setter方法:

- (void)setName:(NSString *)name{
    
    if (![_name isEqualToString:name]) {
        
        [self willChangeValueForKey:@"name"];
        NSLog(@"22---setter");
        _name = [name copy];
        
        [self didChangeValueForKey:@"name"];
    }
}
  1. 打印如下:


    手動調(diào)用`KVO`.gif
利用上述KVO手動調(diào)用的原理, 我們可以監(jiān)聽成員變量. 步驟:

1.添加一個成員變量:

{
    int _age;
}

2.監(jiān)聽:

    [self addObserver:self forKeyPath:@"_age" options:NSKeyValueObservingOptionNew context:nil];

3.實現(xiàn)監(jiān)聽方法:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    if ([keyPath isEqualToString:@"name"]) {
        
        NSLog(@"kvo name = %@", self.name);
        
    }else if ([keyPath isEqualToString:@"_age"]){
        NSLog(@"age = %zd", _age);
    }
    
}

4.添加一個修改_age的事件

///修改age
- (IBAction)modifyAgeAction:(id)sender {
    
    [self willChangeValueForKey:@"_age"];
    
    _age++;
    
    [self didChangeValueForKey:@"_age"];
}

5.打印如下:


利用手動調(diào)用`KVO`,實現(xiàn)監(jiān)聽.gif

context參數(shù)

最后我們再來看下addObserver: forKeyPath:options:context:context參數(shù).它是監(jiān)聽的唯一標(biāo)識,它會被代入監(jiān)聽方法中:observeValueForKeyPath: ofObject: change: context:
通常情況下, 我們不需要 context 參數(shù)來區(qū)別我們的監(jiān)聽, 但是在下面的小概率事件時:

  • 繼承
  • 父類使用了KVO

就需要用到了.

  • 如上述的viewController繼承自BaseViewController
  • BaseViewController也使用到了KVO.
    此時在viewController中的方法observeValueForKeyPath: ofObject: change: context:就覆蓋了父類的實現(xiàn).
    解決方法是:
  • 定義一個唯一的context, 如:
static void *ViewControllerContext = &ViewControllerContext;
  • 監(jiān)聽時,傳入context:
    [self.worker addObserver:self forKeyPath:NSStringFromSelector(@selector(cities)) options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:ViewControllerContext];
  • 在監(jiān)聽方法中,根據(jù)context判斷:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
   
    if (context == ViewControllerContext) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(cities))]) {
            NSLog(@"change info = %@", change);
        }
       
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
  • 如果使用了手動KVO, 也要注意調(diào)用super
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

以上就是我對KVO的總結(jié), 如發(fā)現(xiàn)有欠妥之處, 請隨時指出, 幫助我進(jìn)步, 謝謝.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,688評論 19 139
  • 本文結(jié)構(gòu)如下: Why? (為什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等開會閱讀 1,736評論 1 21
  • 本文由我們團隊的 糾結(jié)倫 童鞋撰寫。 文章結(jié)構(gòu)如下: Why? (為什么要用KVO) What? (KVO是什么...
    知識小集閱讀 7,487評論 7 105
  • 上半年有段時間做了一個項目,項目中聊天界面用到了音頻播放,涉及到進(jìn)度條,當(dāng)時做android時候處理的不太好,由于...
    DaZenD閱讀 3,106評論 0 26
  • 你要知道的KVC、KVO、Delegate、Notification都在這里 轉(zhuǎn)載請注明出處 http://www...
    WWWWDotPNG閱讀 1,923評論 1 3

友情鏈接更多精彩內(nèi)容