描述
KVO全稱
KeyValueObserving。俗稱“鍵值監(jiān)聽”。
利用Key來找到某個(gè)對(duì)象并監(jiān)聽其屬性的改變。也是一種典型的觀察者模式。
在某個(gè)對(duì)象注冊(cè)監(jiān)聽者后,被監(jiān)聽對(duì)象的屬性發(fā)生改變時(shí),會(huì)發(fā)送一個(gè)通知給監(jiān)聽者。以便監(jiān)聽者執(zhí)行回調(diào)操作。

KVO方法介紹
1、通過addObserver:forKeyPath:options:context:方法注冊(cè)觀察者。
/**
添加KVO監(jiān)聽
@param observer 添加觀察者,被觀察者屬性變化通知的目標(biāo)對(duì)象
@param keyPath 監(jiān)聽的屬性路徑
@param options 監(jiān)聽類型 - options支持按位或來監(jiān)聽多個(gè)事件類型
@param context 監(jiān)聽上下文context主要用于在多個(gè)監(jiān)聽器對(duì)象監(jiān)聽相同keyPath時(shí)進(jìn)行區(qū)分
*/
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
2、通過 observeValueForKeyPath:ofObject:change:context:獲得回調(diào),從而做出事件處理。
/**
監(jiān)聽器對(duì)象的監(jiān)聽回調(diào)方法
@param keyPath 監(jiān)聽的屬性路徑
@param object 被觀察者
@param change 監(jiān)聽內(nèi)容的變化
@param context context為監(jiān)聽上下文,由add方法回傳
*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context;
3、當(dāng)觀察者不需要監(jiān)聽時(shí),調(diào)用可以removeObserver:forKeyPath:方法將KVO移除。需要注意的是:調(diào)用removeObserver需要在觀察者消失之前,否則會(huì)導(dǎo)致Crash。
- (void)dealloc{
[self removeObserver:self forKeyPath:@"keyFlag"];
}
簡(jiǎn)單示例
@interface ViewController ()
@property (nonatomic,strong) Animal * ani;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ani = [[Animal alloc] init];
self.ani.age = 10;
// 添加鍵值監(jiān)聽
[self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}
// 點(diǎn)擊事件,觸發(fā)屬性修改
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.ani.age += 5;
}
// 獲得回調(diào),實(shí)時(shí)監(jiān)聽屬性改變、
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"監(jiān)聽到了%@對(duì)象的%@屬性由%@變成了%@屬性",object,keyPath,change,change);
}
// 需要在不使用的時(shí)候,移除監(jiān)聽
- (void)dealloc{
[self.ani removeObserver:self forKeyPath:@"age"];
}
@end
KVO原理探究
1、利用RuntimeAPI動(dòng)態(tài)生成一個(gè)子類
NSKVONotifying_XXX,并且讓當(dāng)前instance對(duì)象的isa指針指向這個(gè)全新子類。
2、當(dāng)修改instance對(duì)象的屬性時(shí),會(huì)觸發(fā)setter方法,調(diào)用Foundation的_NSSetXXXValueAndnotify函數(shù)
- 調(diào)用
willChangeValueForKey:- 調(diào)用原來的
setter實(shí)現(xiàn)(父類原來的setter方法)- 調(diào)用
didChangeValueForKey
此時(shí)內(nèi)部觸發(fā)監(jiān)聽器(Oberser)的監(jiān)聽方法- observeValueForKeyPath: ofObject: change: context:
代碼驗(yàn)證上述流程
第一步:通過runtime查看isa指針指向的 class對(duì)象
如果觀察 Animal的age屬性。
系統(tǒng)會(huì)在運(yùn)行時(shí)生成NSKVONotifying_Animal
在NSKVONotifying_Animal中重寫setter、class、dealloc等方法。
使Animal實(shí)例對(duì)象的isa指針指向NSKVONotifying_Animal
NSKVONotifying_Animal的superclass指向Animal
探究過程
// 注冊(cè)成為觀察者
NSLog(@"添加KVO之前,Animal的class是 = %s",object_getClassName(self.ani));
[self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加KVO之后,Animal的class是 = %s",object_getClassName(self.ani));
結(jié)果如下:
添加KVO之前,Animal的class是 = Animal
添加KVO之后,Animal的class是 = NSKVONotifying_Animal
注冊(cè)成為觀察者之后,類變成了NSKVONotifying_Animal而再是 Animal 。
我們先看一下NSKVONotifying_Animal類內(nèi)部的方法。
#import <objc/runtime.h>
//打印某個(gè)類中的所有方法
- (void)printMethonNamesFromClass:(Class)cls{
unsigned int count;
//獲取方法列表
Method *methodList = class_copyMethodList(cls, &count);
//保存方法名
NSMutableString *methonNames = @"".mutableCopy;
for (int i = 0; i < count; i++) {
//獲取方法
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methonNames appendFormat:@"%@", [NSString stringWithFormat:@"%@, ",methodName]];
}
NSLog(@"methonNames = %@",methonNames);
//c語(yǔ)音創(chuàng)建的list記得釋放
free(methodList);
}
結(jié)果如下:
[self printMethonNamesFromClass:object_getClass(self.ani)];
----------------------------------------------
methonNames = setAge:, class, dealloc, _isKVOA,
畫圖分析KVO內(nèi)部結(jié)構(gòu)


第二步:- (void)setAge:(int)age方法
為了比較在注冊(cè)觀察者前后setter方法的變化,我們新創(chuàng)建一個(gè)實(shí)例ani1
self.ani = [[Animal alloc] init];
self.ani1 = [[Animal alloc] init];
NSLog(@"添加KVO之前,ani的setAge是 = %p,未添加KVO的ani1的setAge是 = %p",
[self.ani methodForSelector:@selector(setAge:)],
[self.ani1 methodForSelector:@selector(setAge:)]);
// 注冊(cè)成為觀察者
[self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加KVO之后,ani的setAge是 = %p,未添加KVO的ani1的setAge是 = %p",
[self.ani methodForSelector:@selector(setAge:)],
[self.ani1 methodForSelector:@selector(setAge:)]);
結(jié)果如下:
添加KVO之前,ani的setAge是 = 0x10d751460,未添加KVO的ani1的setAge是 = 0x10d751460
添加KVO之后,ani的setAge是 = 0x10daaacf2,未添加KVO的ani1的setAge是 = 0x10d751460
這里可以看到,添加KVO前后,setAge方法有所改變
我們進(jìn)入debugger來看看這第這個(gè)方法的實(shí)現(xiàn)到底是怎樣的:
(gdb) print (IMP) 0x10daaacf2
$1 = (IMP) 0x96a1a550 <_NSSetIntValueAndNotify>
原來在重寫的NSKVONotifying_Animal的-setAge方法中會(huì)調(diào)用_NSSetIntValueAndNotify:
// 注:Foundation框架中類似_NSSetIntValueAndNotify的方法實(shí)現(xiàn)還有很多:
__NSSetBoolValueAndNotify
__NSSetCharValueAndNotify
__NSSetDoubleValueAndNotify
__NSSetFloatValueAndNotify
__NSSetIntValueAndNotify
__NSSetLongLongValueAndNotify
__NSSetLongValueAndNotify
__NSSet0bjectValueAndNotify
__NSSetPointValueAndNotify
__NSSetRangeValueAndNotify
__NSSetRectValueAndNotify
__NSSetShortValueAndNotify
__NSSetSizeValueAndNotify
查看_NSSet*ValueAndNotify的內(nèi)部實(shí)現(xiàn)
- (void)setAge:(int)age{
_NSSet*ValueAndNotify();
}
// 因?yàn)開NSSetIntValueAndNotify在Foundation框架中,無法查看起具體實(shí)現(xiàn),根據(jù)實(shí)踐猜測(cè)大致為代碼如下:
void _NSSet*ValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
//通過監(jiān)聽器,監(jiān)聽屬性發(fā)生了改變
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
在Animal類中驗(yàn)證
#import "Animal.h"
@implementation Animal
//Animal內(nèi)部代碼實(shí)現(xiàn)
- (void)setAge:(int)age{
_age = age;
NSLog(@"setAge");
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey == begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey == end");
}
@end
結(jié)果如下:
kvoAndkvoDemo[2391:50226] willChangeValueForKey
kvoAndkvoDemo[2391:50226] setAge
kvoAndkvoDemo[2391:50226] didChangeValueForKey == begin
kvoAndkvoDemo[2391:50226] 監(jiān)聽到了<Animal: 0x600000da3690>對(duì)象的age屬性由{
kind = 1;
new = 15;
old = 10;
}變成了{(lán)
kind = 1;
new = 15;
old = 10;
}屬性
kvoAndkvoDemo[2391:50226] didChangeValueForKey == end
如果不添加監(jiān)聽,則不會(huì)執(zhí)行
willChangeValueForKey和didChangeValueForKey方法、驗(yàn)證成功!
匯總
1、當(dāng)你觀察一個(gè)對(duì)象時(shí),系統(tǒng)通過Runtime動(dòng)態(tài)的創(chuàng)建一個(gè)該類的派生類,這個(gè)類繼承自該對(duì)象的原本的類,并了重寫被觀察屬性的setter方法。
2、isa指針會(huì)指向這個(gè)新創(chuàng)建的類,該對(duì)象就變成新創(chuàng)建子類的實(shí)例了、
3、重寫的setter方法,執(zhí)行_NSSet*ValueAndNotify,會(huì)負(fù)責(zé)在調(diào)用原來的setter方法前后,通知所有觀察對(duì)象:值的改變。

拓展思考
1、用法聽起來和
NSNotification很相似啊, 其實(shí)NSNotification也是觀察者模式,但是NSNotification是一種廣播機(jī)制,KVO是被觀察者直接發(fā)消息給觀察者,是對(duì)象間的相互溝通。NSNotification則是兩者都和通知中心對(duì)象交互,對(duì)象之間不知道彼此。
2、KVO行為是同步的,并且發(fā)生與觀察的值發(fā)生在同樣的線程上,沒有隊(duì)列或Run-Loop處理。【使用注意】
用途
常見運(yùn)用是監(jiān)聽
ScrollView的contentOffset屬性。當(dāng)用戶滾動(dòng)結(jié)束時(shí)動(dòng)態(tài)改變某些空間的實(shí)現(xiàn)效果。下拉刷新,漸變導(dǎo)航欄,頭像變大縮小等。
KVC
KVC (Key-Value-Coding )鍵值編碼。顧名思義:可以通過一個(gè)Key來訪問某個(gè)屬性。
常用方法
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *) keyPath;
簡(jiǎn)單示例
self.ani = [[Animal alloc] init];
[self.ani setValue:@38 forKey:@"age"];
NSLog(@"%d",self.ani.age);
self.ani1 = [[Animal alloc] init];
[self.ani1 setValue:@99 forKeyPath:@"cat.weight"];
NSLog(@"%d",self.ani1.cat.weight);
self.ani2 = [[Animal alloc] init];
self.ani2.age = 10;
NSLog(@"%@",[self.ani2 valueForKey:@"age"]);
setValue:forKey:的原理:

當(dāng)我們?cè)O(shè)置setValue:forKey:時(shí)
首先會(huì)查找setKey:、_setKey: (按順序查找)
如果有直接調(diào)用
如果沒有,先查看accessInstanceVariablesDirectly方法
如果可以訪問會(huì)按照 _key、_isKey、key、iskey的順序查找成員變量
找到直接復(fù)制
未找到報(bào)錯(cuò)NSUnkonwKeyException錯(cuò)誤
valueForKey:的原理:

kvc取值按照 getKey、key、iskey、_key 順序查找方法
存在直接調(diào)用
沒找到同樣,先查看accessInstanceVariablesDirectly方法
如果可以訪問會(huì)按照 _key、_isKey、key、iskey的順序查找成員變量
找到直接復(fù)制
未找到報(bào)錯(cuò)NSUnkonwKeyException錯(cuò)誤
思考
我們可以通過 self.ani.age = 10; 來賦值,也可通過上述代碼進(jìn)行賦值,看著多此一舉、
可是如果人這個(gè)類的屬性是沒有暴露在外面呢?比如現(xiàn)在給人這個(gè)類一個(gè)私有的身高的屬性。就可以通過KVC進(jìn)行賦值、
Key 和 KeyPath區(qū)別接聯(lián)系
Key:只能訪問當(dāng)前對(duì)象的屬性,如果按路徑找會(huì)報(bào)錯(cuò)。
KeyPath:相當(dāng)于根據(jù)路徑去尋找屬性,能利用運(yùn)算符一層一層往內(nèi)部訪問屬性。
用途
我們通過KVC可以直接對(duì)私有屬性并進(jìn)行賦值
字典轉(zhuǎn)模型
拓展
我們通過
XIB或者SB拖線布局連線錯(cuò)誤的時(shí)候也會(huì)報(bào)錯(cuò)說找不到什么key,說明Storyboard在賦值的時(shí)候也是通過KVC來操作的。
試題
KVO相關(guān):
1. iOS用什么方式來實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO?(KVO的本質(zhì)是什么?)
2. 如何手動(dòng)出發(fā)KVO?
3. 直接修改成員變量會(huì)觸發(fā)KVO么?
KVC相關(guān):
1. 通過KVC修改屬性會(huì)觸發(fā)KVO么?
2. KVC的賦值和取值過程是怎樣的?原理是什么?