介紹
工程中我們常常需要得到成員變量或屬性的值的改變, 在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 的使用
步驟
- 添加監(jiān)聽:
addObserver: forKeyPath: options: context: - 實現(xiàn)監(jiān)聽方法:
observeValueForKeyPath: ofObject: change: context: - 移除監(jiān)聽:
removeObserver: forKeyPath:
示例
- 在
ViewController創(chuàng)建一個屬性
@property (nonatomic, copy) NSString *name;
- 添加
key-value-observer
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
- 實現(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);
}
}
- 在
ViewController的dealloc中移除
- (void)dealloc{
[self removeObserver:self forKeyPath:@"name"];
}
- 注: 移除
observer視實際情況而定, 也可以在viewDidDisappear:或者處理完監(jiān)聽, 在observeValueForKeyPath: ofObject: change: context:最后.
測試
- 在
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
- 點擊按鈕, 更改
name屬性, 可以看到KVO的監(jiān)聽方法被觸發(fā):
KVO觸發(fā).gif
以上即為KVO的基本使用, 也是系統(tǒng)的自動調(diào)用. KVO自動調(diào)用的原理為:
- 系統(tǒng)會重寫被監(jiān)聽屬性的
setter方法, 如上述的setName:, 所以, 必須監(jiān)聽屬性, 有setter方法 - 系統(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)用, 就有手動調(diào)用, 手動調(diào)用我們將在后面講述.
監(jiān)聽一個屬性, 實現(xiàn)監(jiān)聽多個屬性
我們使用間接屬性來舉例
- 定義一個
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;
-
Child.m中, 初始化上述屬性:
- (instancetype)init{
if (self = [super init]) {
self.birthday = @"2000-01-01";
self.year = 2000;
self.month = 1;
self.day = 1;
}
return self;
}
- 定義一個
Worker類, 它有一個Child屬性:
@property (nonatomic, strong) Child *child;
-
viewController類中添加一個worker屬性:
@property (nonatomic, strong) Worker *worker;
- 監(jiān)聽worker 的child 中birthday 的改變:
[self.worker addObserver:self forKeyPath:@"child.birthday" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
- 添加更改間接屬性的事件:
//更改間接屬性值事件
- (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);
}
}

- 上例中, 對于
Child來說, 其屬性birthday是由year, month, day影響的, 即當(dāng)year, month, day其一改變時, 關(guān)心birthday的外界也需要收到監(jiān)聽. 這種情況下, 當(dāng)Child的year, 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):
- 被監(jiān)聽的對象需要實現(xiàn)下面方法
- 且操作數(shù)組屬性時, 也要使用下面對應(yīng)的方法:
- objectInMyArrayAtIndex:
- insertObject:inMyArrayAtIndex:
- removeObjectFromMyArrayAtIndex:
- replaceObjectInMyArrayAtIndex:withObject:
同KVO的其它方法一樣, 重寫這些方法時, 系統(tǒng)也會有補全提示, 而上述中的MyArray會替換成實際的屬性名稱.
- 依然在上述例子中, 我們?yōu)?code>worker添加一個
cities屬性:
@property (nonatomic, strong) NSMutableArray *cities;
- 在
Worker.m中初始化:
- (instancetype)init{
if (self = [super init]) {
self.cities = [NSMutableArray array];
}
return self;
}
- 實現(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];
}
- 并在
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;
- 在
viewController中監(jiān)聽:
[self.worker addObserver:self forKeyPath:NSStringFromSelector(@selector(cities)) options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
- 依次執(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)用
- 重寫觸發(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
- 在
name改變, 需要觸發(fā)監(jiān)聽方法observeValueForKeyPath: ofObject: change: context:時, 手動調(diào)用-willChangeValueForKey:及- didChangeValueForKey:
實現(xiàn)
-
把我們的名字?jǐn)?shù)組的
李四變成張三, 這樣我們就有兩個張三了:
兩個`張三.png 重寫
setter方法:
- (void)setName:(NSString *)name{
if (![_name isEqualToString:name]) {
[self willChangeValueForKey:@"name"];
NSLog(@"22---setter");
_name = [name copy];
[self didChangeValueForKey:@"name"];
}
}
-
打印如下:
手動調(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.打印如下:

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];
}


