1. 程式人生 > >詳細的KVO總結,包括基本改變,使用案例,注意點.看我就夠了!

詳細的KVO總結,包括基本改變,使用案例,注意點.看我就夠了!

概述

  1. KVO全稱Key-Value-Observing,也叫鍵值監聽,是一種觀察者設計模式.提供了一種機制,當指定的物件的屬性被修改後,物件就會收到一個通知.也就是說每次指定的被觀察的物件的屬性被修改後,KVO就會自動通知相應的觀察者.
  2. 優勢:可以降低兩個類(業務邏輯和檢視控制的類)之間的耦合性.也就是說可以很容易的實現檢視元件和資料模型的分離.當資料模型的屬性值改變之後作為監聽器的檢視元件就會被激發,激發時就會回撥監聽器自身.
  3. 在Objective-C中要實現KVO則必須實現NSKeyValueObServing協議.但不用擔心,因為NSObject已經實現了該協議,因此幾乎所有的Objective-C物件都可以使用KVO.

KVO的方法

  1. 監聽方法
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    某一個物件(受虐狂,喜歡被人監視),給自己新增一個監聽者(一般都是控制器本身self),讓監聽者監聽自身的某一個屬性. options就是要求監聽者記錄的資訊. context就是要監聽者給自己新增一個標記,以防止和別的物件的監聽混淆. 比如: 有兩個孩子讓家長監聽他們做作業.監聽者是家長,被監聽的物件是兩個孩子.
    引數:
  • observer 觀察者,也就是KVO的訂閱者,訂閱者必須實現協議方法(下面有).
  • keyPath 描述將要觀察的物件的屬性,也就是被觀察者的屬性.
  • options KVO的屬性配置.
    • NSKeyValueObservingOptionNewchange字典包括改變後的值
    • NSKeyValueObservingOptionOldchange字典包括改變前的值
    • NSKeyValueObservingOptionInitial註冊後立刻觸發KVO通知
    • NSKeyValueObservingOptionPrior值改變前是否也要通知(這個key決定了是否在改變前改變後通知兩次)
  • context上下文,這個會傳遞到協議方法中,用來區分訊息,處理不同的KVO.所以應當是不同的.
  1. 解除監聽
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; 刪除指定keyPath的監聽器.
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);刪除特定上下文標記的指定keyPath的監聽器.
  2. 回撥監聽
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
    引數:
    • keyPath 被監聽的keyPath
    • object 被監聽的修改後的物件,可以獲取修改的物件的屬性
    • change 儲存資訊改變的字典(可能有舊的值,新的值等
    • context 上下文

使用步驟

  1. 註冊KVO監聽.
  2. 實現代理方法.
  3. 移除監聽.在dealloc方法中移除.

KVO使用注意事項

非常重要

  1. 當你在同一個ViewController中新增多個KVO的時候,無論哪個KVO都是走- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;回撥方法.所以需要對想要的監聽物件進行區分,以便指定不同的邏輯.
    這裡是對_tableView物件的contentOffset屬性監聽.
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) { [self doSomething]; } }
  2. 我們假設當前類(在例子中為UITableViewController)還有父類,並且父類也有自己綁定了一些其他KVO呢?我們看到,上述回撥函式體中只有一個判斷,如果這個if不成立,這次KVO事件的觸發就會到此中斷了。但事實上,若當前類無法捕捉到這個KVO,那很有可能是在他的superClass,或者super-superClass...中,上述處理砍斷了這個鏈。合理的處理方式應該是這樣的:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) { [self doSomethingWhenContentOffsetChanges]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
但是這個是要自己搞清楚,父類中到底有沒有註冊KVO.如果監聽一個物件的兩個屬性,兩個屬性的改變時分開執行的,就會觸發兩次代理方法.如圖:

1.png

  1. KVO的一個特性,當對同一個keyPath進行多餘一次的removeObserver的時候會導致程式crash.這種情況常常出現在父類有一個kvo,父類在dealloc中remove了一次,子類又remove了一次的情況下。不要以為這種情況很少出現!當你封裝framework開源給別人用或者多人協作開發時是有可能出現的,而且這種crash很難發現.解決辦法就是我們可以分別在父類以及本類中定義各自的context字串,這樣iOS就能知道移除的是自己的kvo,而不是父類中的kvo,避免二次remove造成crash.
  2. 把監聽到物件的屬性值改變賦值的時候,一定要注意監聽物件的值的型別.
    把監聽到物件的屬性值改變賦值的時候,一定要注意監聽物件的值的型別.
    把監聽到物件的屬性值改變賦值的時候,一定要注意監聽物件的值的型別.
    重要的事情說三遍
  3. 如果監聽一個物件的多個屬性,任何一個屬性的改變都會走代理方法,也就是說對屬性的監聽,是分開執行的.

全部程式碼

2.png

  1. MCBuyData.h
    #import <Foundation/Foundation.h> @interface MCBuyData : NSObject @property (nonatomic, assign) NSInteger number; @property (nonatomic, assign) NSInteger money; @end
  2. MCBuyData.m
    #import "MCBuyData.h" @implementation MCBuyData @end
  3. ViewController.h
    #import <UIKit/UIKit.h> @class MCBuyData; @interface ViewController : UIViewController @property (nonatomic, strong) MCBuyData * buyData; @end
  4. ViewController.m

    #import "ViewController.h"
    #import "Masonry.h"
    #import "MCBuyData.h"
    #define kNumber @"number"
    #define kMoney @"money"

@interface ViewController ()

@property (nonatomic, strong) UILabel * numberLabel;
@property (nonatomic, strong) UILabel * moneyLabel;
@property (nonatomic, strong) UIButton * toBuyButton;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self settingObserver];
    [self initUI];
}
- (void)dealloc {
    [self.buyData removeObserver:self forKeyPath:kNumber context:@"number"];
    [self.buyData removeObserver:self forKeyPath:kMoney context:@"money"];
}
#pragma mark - 點選事件
- (void)toBuyButtonClicked {
    NSInteger number = [[self.buyData valueForKey:kNumber] integerValue];
    number += 1;
    [self.buyData setValue:@(number) forKey:kNumber];
    NSInteger money = [[self.buyData valueForKey:kMoney] integerValue];
    money += 100;
    [self.buyData setValue:@(money) forKey:kMoney];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    // 該change的內容記錄的是本次監聽到的屬性的改變.
    NSLog(@"change: %@",change);
    NSString * new = change[@"new"];
    if (object == self.buyData && [keyPath isEqualToString:kNumber] && (context == @"number")) {
        self.numberLabel.text = [NSString stringWithFormat:@"次數: %@",new];
    } else {
        // 寫了這句,如果父檢視中沒有註冊的KVO,就會崩掉.
        // reason: '<ViewController: 0x7fd7af406030>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
     //   [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    if ([keyPath isEqualToString:kMoney]) {
        self.moneyLabel.text = [NSString stringWithFormat:@"金額: %@",new];
    } else {
       // [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
- (void)settingObserver {
    self.buyData = [[MCBuyData alloc] init];
    [self.buyData setValue:@(0) forKey:kNumber];
    [self.buyData setValue:@(0) forKey:kMoney];
   [self.buyData addObserver:self forKeyPath:kNumber options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"number"];
    [self.buyData addObserver:self forKeyPath:kMoney options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"money"];
}
- (void)initUI {
    [self.view addSubview:self.numberLabel];
    [self.numberLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(self.view).with.offset(20);
        make.right.mas_equalTo(self.view).with.offset(-20);
        make.top.mas_equalTo(self.view).with.offset(100);
        make.height.mas_equalTo(50);
    }];
    [self.view addSubview:self.moneyLabel];
    [self.moneyLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(self.view).with.offset(20);
        make.right.mas_equalTo(self.view).with.offset(-20);
        make.top.mas_equalTo(self.view).with.offset(250);
        make.height.mas_equalTo(50);
    }];
    [self.view addSubview:self.toBuyButton];
    [self.toBuyButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(self.view).with.offset(20);
        make.right.mas_equalTo(self.view).with.offset(-20);
        make.top.mas_equalTo(self.view).with.offset(400);
        make.height.mas_equalTo(50);
    }];
}
#pragma mark - setter & getter 
- (UILabel *)numberLabel {
    if (_numberLabel == nil) {
        self.numberLabel = [[UILabel alloc] init];
        self.numberLabel.backgroundColor = [UIColor orangeColor];
        self.numberLabel.font = [UIFont systemFontOfSize:15];
        self.numberLabel.textColor = [UIColor whiteColor];
        self.numberLabel.textAlignment = NSTextAlignmentCenter;
        self.numberLabel.text = @"次數: 1";
    } return _numberLabel;
}
- (UILabel *)moneyLabel {
    if (_moneyLabel == nil) {
        self.moneyLabel = [[UILabel alloc] init];
        self.moneyLabel.backgroundColor = [UIColor orangeColor];
        self.moneyLabel.font = [UIFont systemFontOfSize:15];
        self.moneyLabel.textColor = [UIColor whiteColor];
        self.moneyLabel.textAlignment = NSTextAlignmentCenter;
        self.moneyLabel.text = @"金額: 1";
    } return _moneyLabel;
}
- (UIButton *)toBuyButton {
    if (_toBuyButton == nil) {
        self.toBuyButton = [UIButton buttonWithType:UIButtonTypeCustom];
        self.toBuyButton.titleLabel.font = [UIFont systemFontOfSize:14];
        self.toBuyButton.backgroundColor = [UIColor redColor];
        [self.toBuyButton setTitle:@"買 買 買!!!" forState:UIControlStateNormal];
        [self.toBuyButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [self.toBuyButton addTarget:self action:@selector(toBuyButtonClicked) forControlEvents:UIControlEventTouchUpInside];
    } return _toBuyButton;
}
@end

Demo 下載地址

https://github.com/mancongiOS/KVO.git

說明

  1. KVO注意事項1,2,3條轉載於 程式設計小翁@部落格園