詳細的KVO總結,包括基本改變,使用案例,注意點.看我就夠了!
阿新 • • 發佈:2019-02-15
概述
- KVO全稱Key-Value-Observing,也叫鍵值監聽,是一種觀察者設計模式.提供了一種機制,當指定的物件的屬性被修改後,物件就會收到一個通知.也就是說每次指定的被觀察的物件的屬性被修改後,KVO就會自動通知相應的觀察者.
- 優勢:可以降低兩個類(業務邏輯和檢視控制的類)之間的耦合性.也就是說可以很容易的實現檢視元件和資料模型的分離.當資料模型的屬性值改變之後作為監聽器的檢視元件就會被激發,激發時就會回撥監聽器自身.
- 在Objective-C中要實現KVO則必須實現NSKeyValueObServing協議.但不用擔心,因為NSObject已經實現了該協議,因此幾乎所有的Objective-C物件都可以使用KVO.
KVO的方法
- 監聽方法
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
某一個物件(受虐狂,喜歡被人監視),給自己新增一個監聽者(一般都是控制器本身self),讓監聽者監聽自身的某一個屬性. options就是要求監聽者記錄的資訊. context就是要監聽者給自己新增一個標記,以防止和別的物件的監聽混淆. 比如: 有兩個孩子讓家長監聽他們做作業.監聽者是家長,被監聽的物件是兩個孩子.
引數:
observer
觀察者,也就是KVO的訂閱者,訂閱者必須實現協議方法(下面有).keyPath
描述將要觀察的物件的屬性,也就是被觀察者的屬性.options
KVO的屬性配置.NSKeyValueObservingOptionNew
change字典包括改變後的值NSKeyValueObservingOptionOld
change字典包括改變前的值NSKeyValueObservingOptionInitial
註冊後立刻觸發KVO通知NSKeyValueObservingOptionPrior
值改變前是否也要通知(這個key決定了是否在改變前改變後通知兩次)
context
上下文,這個會傳遞到協議方法中,用來區分訊息,處理不同的KVO.所以應當是不同的.
- 解除監聽
- (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的監聽器. - 回撥監聽
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
引數:keyPath
被監聽的keyPathobject
被監聽的修改後的物件,可以獲取修改的物件的屬性change
儲存資訊改變的字典(可能有舊的值,新的值等context
上下文
使用步驟
- 註冊KVO監聽.
- 實現代理方法.
- 移除監聽.在dealloc方法中移除.
KVO使用注意事項
非常重要
- 當你在同一個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]; } }
- 我們假設當前類(在例子中為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.如果監聽一個物件的兩個屬性,兩個屬性的改變時分開執行的,就會觸發兩次代理方法.如圖:
- KVO的一個特性,當對同一個
keyPath
進行多餘一次的removeObserver
的時候會導致程式crash.這種情況常常出現在父類有一個kvo,父類在dealloc中remove了一次,子類又remove了一次的情況下。不要以為這種情況很少出現!當你封裝framework開源給別人用或者多人協作開發時是有可能出現的,而且這種crash很難發現.解決辦法就是我們可以分別在父類以及本類中定義各自的context字串,這樣iOS就能知道移除的是自己的kvo,而不是父類中的kvo,避免二次remove造成crash. - 把監聽到物件的屬性值改變賦值的時候,一定要注意監聽物件的值的型別.
把監聽到物件的屬性值改變賦值的時候,一定要注意監聽物件的值的型別.
把監聽到物件的屬性值改變賦值的時候,一定要注意監聽物件的值的型別.
重要的事情說三遍 - 如果監聽一個物件的多個屬性,任何一個屬性的改變都會走代理方法,也就是說對屬性的監聽,是分開執行的.
全部程式碼
- MCBuyData.h
#import <Foundation/Foundation.h> @interface MCBuyData : NSObject @property (nonatomic, assign) NSInteger number; @property (nonatomic, assign) NSInteger money; @end
- MCBuyData.m
#import "MCBuyData.h" @implementation MCBuyData @end
- ViewController.h
#import <UIKit/UIKit.h> @class MCBuyData; @interface ViewController : UIViewController @property (nonatomic, strong) MCBuyData * buyData; @end
-
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
說明
- KVO注意事項1,2,3條轉載於 程式設計小翁@部落格園