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

概述git

  1. KVO全稱Key-Value-Observing,也叫鍵值監聽,是一種觀察者設計模式.提供了一種機制,當指定的對象的屬性被修改後,對象就會收到一個通知.也就是說每次指定的被觀察的對象的屬性被修改後,KVO就會自動通知相應的觀察者.
  2. 優點:能夠下降兩個類(業務邏輯和視圖控制的類)之間的耦合性.也就是說能夠很容易的實現視圖組件和數據模型的分離.當數據模型的屬性值改變以後做爲監聽器的視圖組件就會被激發,激發時就會回調監聽器自身.
  3. 在Objective-C中要實現KVO則必須實現NSKeyValueObServing協議.但不用擔憂,由於NSObject已經實現了該協議,所以幾乎全部的Objective-C對象均可以使用KVO.

KVO的方法github

  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.因此應當是不一樣的.
  2. 解除監聽
    - (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的監聽器.
  3. 回調監聽
    - (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使用注意事項設計模式

很是重要ide

  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. 若是監聽一個對象的多個屬性,任何一個屬性的改變都會走代理方法,也就是說對屬性的監聽,是分開執行的.

所有代碼atom

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 下載地址code

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

說明

  1. KVO注意事項1,2,3條轉載於 編程小翁@博客園
相關文章
相關標籤/搜索