KVO (Key-Value Observing)盯漂,俗稱“鍵值監(jiān)聽”,能夠用來監(jiān)聽對象屬性的變化,也是 Objective-C 中的“觀察者模式”最典型的實現(xiàn)缴饭。
KVO
基本使用
有如下 Valenti 定義:
@interface Valenti : NSObject
@property(nonatomic, assign) NSInteger age;
@end
注冊通知
外部:
聲明全局靜態(tài)變量 Context:
static void * ValentiObserverContext = &ValentiObserverContext;
_v = [[Valenti alloc] init];
_v.age = 23;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[_v addObserver:self forKeyPath:@"age" options:options context: ValentiObserverContext];
其中 - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
為注冊方法硫椰,也就是對 _v 注冊監(jiān)聽繁调。
keyPath: 目標觀察屬性
options: 監(jiān)聽事件的類型,可用位運算進行多個類型的監(jiān)聽
context: 上下文靶草,可對多個監(jiān)聽器對象監(jiān)聽相同 keyPath 進行區(qū)分
在這里需要注意的是很多人都喜歡將 context 參數(shù)傳 nil蹄胰,這并不是恰當?shù)淖龇ǎ袝r我們通過 keyPath 來判斷是無法得知 Valenti 的父類是否也在監(jiān)聽該對象奕翔,通過 context 判斷就不會有這個問題裕寨,并且蘋果推薦的 context 應該是獨一無二且每個屬性都應該有一個 context 的,字符串不能保證獨一無二派继,那么最正確的做法就是用一個靜態(tài)變量的地址作為 context 傳值宾袜。形如:
static void * ValentiObeserverContext = &ValentiObserverContext;
options 最常見的傳值便是:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
,表示對新舊值都監(jiān)聽驾窟,options 總共有四個:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
NSKeyValueObservingOptionInitial:不會監(jiān)聽屬性任何值庆猫,但會在注冊監(jiān)聽的那一刻立即發(fā)送通知
NSKeyValueObservingOptionPrior:會在值改變前發(fā)送一次通知,纫普,值改變后發(fā)送一次通知阅悍,并且,值改變前的通知中change
字典里會包含一個鍵值對notificationIsPrior = 1;
實現(xiàn)方法
注冊通知后需要實現(xiàn) - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context
方法以便觀察對結(jié)果的監(jiān)聽:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
NSLog(@"屬性 %@", keyPath);
NSLog(@"改變后的值 %@", change);
NSLog(@"上下文 %p", context);
}
將年齡修改為 26 的時候觸發(fā)方法調(diào)用昨稼。
打印結(jié)果:
屬性 age
改變后的值 {
kind = 1;
new = 26;
old = 23;
}
上下文 0x103a00098
{}
內(nèi)就是 change 的內(nèi)容节视。
移除通知
一般情況下是在 dealloc
方法中移除監(jiān)聽,方法為 - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
:
- (void)dealloc {
[_v removeObserver:self forKeyPath:@"age"];
}
探究本質(zhì)
上述代碼稍做改變:
初始化增加 v2 對象假栓。
_v = [[Valenti alloc] init];
_v.age = 23;
_v2 = [[Valenti alloc] init];
_v2.age = 1;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[_v addObserver:self forKeyPath:@"age" options:options context: ValentiObserverContext];
修改屬性同時增加對 v2 的 age 屬性的修改寻行。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
_v.age = 26;
_v2.age = 100;
}
運行結(jié)果:
屬性 age
改變后的值 {
kind = 1;
new = 26;
old = 23;
}
上下文 0x10b88c098
看到僅僅是監(jiān)聽到了 v 的變化,畢竟我僅僅對 v 進行了監(jiān)聽而并沒有對 v2 注冊監(jiān)聽匾荆。
但是問題來了拌蜘,xx.age = xxx;
相當于調(diào)用屬性的 setter
方法杆烁,即相當于:
[_v setAge: 26];
[_v2 setAge: 100];
并且手動實現(xiàn) age 的 setter 方法:
- (void)setAge:(NSInteger)age {
_age = age;
}
運行后發(fā)現(xiàn)只有在 v 的 age 屬性賦值后才出發(fā)了監(jiān)聽的方法,而同樣對 age 賦值的 v2 卻沒有觸發(fā)通知简卧,那么問題不出現(xiàn)在 setter 方法上兔魂,所以可推測問題只出現(xiàn)在對象本身身上。
借助 Xcode 調(diào)試
我們在 [_v setAge: 26]
加斷點調(diào)試举娩,運行進入 LLDB 調(diào)試環(huán)境打印 self.v.isa
指針析校,如圖:
發(fā)現(xiàn) v 的 isa 指針指向 NSKVONotifying_Valenti
,同理打印 v2 的 isa 指針铜涉,結(jié)果為:
v2 的 isa 指針指向的是
Valenti
智玻。
我們在對象 v 添加監(jiān)聽之前和之后分別打印 object_getClass()
返回結(jié)果:
運行結(jié)果為:
Valenti
NSKVONotifying_Valenti
再次驗證,在 NSObject 對象本質(zhì)中得知實例對象的 isa 指針指向其類對象芙代,那么此處 v 的 isa 指針指向的 NSKVONotifying_Valenti 就是其類對象吊奢,這和 v2 的差異就顯示出來了。
這個“神奇”的 v 對象被系統(tǒng)“動了手腳”——改了其類對象纹烹!
此時再來借助 methodForSelector()
打印 v 和 v2 在添加監(jiān)聽之前和添加監(jiān)聽之后的 setAge: 實現(xiàn)的地址:
NSLog(@"v---->%p, v2---->%p", [self.v methodForSelector: @selector(setAge:)], [self.v2 methodForSelector: @selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[_v addObserver:self forKeyPath:@"age" options:options context: ValentiObserverContext];
NSLog(@"v---->%p, v2---->%p", [self.v methodForSelector: @selector(setAge:)], [self.v2 methodForSelector: @selector(setAge:)]);
運行結(jié)果為:
v---->0x1003ea944, v2---->0x1003ea944
v---->0x1f72e90f8, v2---->0x1003ea944
methodForSelector()
返回的是一個實現(xiàn)页滚,也就是 IMP 類型。
發(fā)現(xiàn) v 在添加監(jiān)聽之后的實現(xiàn)已然和另外三個地址不同滔韵,那這個不同的地址的實現(xiàn)又是如何的逻谦,再進入 LLDB 調(diào)試環(huán)境下打印其實現(xiàn),借助命令:
po IMP(地址)
打印 0x1003ea944 和 0x1f72e90f8 兩個實現(xiàn)陪蜻,結(jié)果為:
發(fā)現(xiàn)未添加監(jiān)聽的 v2 的 setAge: 方法和添加監(jiān)聽前 v 的 setAge: 都是 Valenti 的 setAge: 方法的實現(xiàn)邦马,而添加了監(jiān)聽后的 v 的 setAge: 的實現(xiàn)卻是 Foundation 框架下的 _NSSetLongLongValueAndNotify
方法。
進而得知宴卖,KVO 是運用 Runtime 機制換了 setter 方法的實現(xiàn)滋将。那么 v 和 Valenti 以及 NSKVONotifying_Valenti 的關(guān)系是什么?通過 Objetive-C 對象的分類以及 isa症昏、superclass 指針的方法可以打印 NSKVONotifying_Valenti 的父類随闽,結(jié)果如下:
原來 NSKVONotifying_Valenti 是 Valenti 的子類。
那么肝谭,動態(tài)生成的 NSKVONotifying_Valenti 中有什么實現(xiàn)方法掘宪?運行下段:
unsigned int count = 0;
Method *methodArray = class_copyMethodList(object_getClass(_v), &count);
unsigned int i;
for(i = 0; i < count; i++) {
NSLog(@"----->%@", NSStringFromSelector(method_getName(methodArray[i])));
}
free(methodArray);
得到打印:
----->setAge:
----->class
----->dealloc
----->_isKVOA
可推測在對 age 屬性進行賦值的時候攘烛,調(diào)用流程為:通過 isa 指針找到 NSKVONotifying_Valenti 類對象魏滚,在該類對象中找有關(guān) setAge: 的實現(xiàn)然后進行調(diào)用,并不是直接找到 Valenti 的類對象中的 setAge: 進行調(diào)用坟漱。接著鼠次,NSKVONotifying_Valenti 類對象的 setAge: 又調(diào)用了 Foundation 框架的 _NSSetIntValueAndNotify
C 語言函數(shù),通過逆向技術(shù)可得到在 _NSSetIntValueAndNotify 函數(shù)中還有 willChangeValueForKey:
方法和 didChangeValueForKey:
,這幾個函數(shù)的調(diào)用關(guān)系為:
最后在 didChangeValueForKey:
函數(shù)中調(diào)用監(jiān)聽器方法腥寇,通知 age 屬性發(fā)生了改變成翩。
對 Foundation 框架使用
nm -a
命令(逆向技術(shù)得到 Foundation 框架)可以獲得其私有方法列表,如下:
0013df80 t __NSSetBoolValueAndNotify
000a0480 t __NSSetCharValueAndNotify
0013e120 t __NSSetDoubleValueAndNotify
0013e1f0 t __NSSetFloatValueAndNotify
000e3550 t __NSSetIntValueAndNotify
0013e390 t __NSSetLongLongValueAndNotify
0013e2c0 t __NSSetLongValueAndNotify
00089df0 t __NSSetObjectValueAndNotify
0013e6f0 t __NSSetPointValueAndNotify
0013e7d0 t __NSSetRangeValueAndNotify
0013e8b0 t __NSSetRectValueAndNotify
0013e550 t __NSSetShortValueAndNotify
0008ab20 t __NSSetSizeValueAndNotify
0013e050 t __NSSetUnsignedCharValueAndNotify
0009fcd0 t __NSSetUnsignedIntValueAndNotify
0013e470 t __NSSetUnsignedLongLongValueAndNotify
0009fc00 t __NSSetUnsignedLongValueAndNotify
0013e620 t __NSSetUnsignedShortValueAndNotify
其他 3 個方法
在上文中得知赦役,v 添加注冊監(jiān)聽后會動態(tài)添加新類 NSKVONotifying_Valenti麻敌,并且這個類除了實現(xiàn) setter 方法外,還有 class 方法
掂摔、dealloc 方法
以及 _isKVOA 方法
庸论。
dealloc
dealloc 的作用就是移除自身的監(jiān)聽或者其他代理相關(guān)。
class
已經(jīng)知道棒呛,class
方法和 object_getClass:
方法都能獲得其類對象:
NSLog(@"%@ %@", object_getClass(_v), [_v class]);
理論上得到的結(jié)果都應該是 Valenti 或者 NSKVONotifying_Valenti,但實際卻不相同:
NSKVONotifying_Valenti Valenti
Runtime 函數(shù)打印的是動態(tài)生成的類域携,也就是 v 的 isa 指針真正指向的類對象簇秒,而 class 方法返回的卻是 Valenti 類,那么可猜想秀鞭,在 NSKVONotifying_Valenti 中的 class 方法返回的很有可能是 [Valenti class]
以至于“騙”過我們讓我們以為沒有 NSKVONotifying_Valenti 的存在趋观!
_isKVOA
從命名來看該方法應該是標識該對象是否被添加了監(jiān)聽的標志。
結(jié)論
到這里已經(jīng)明白锋边,KVO 的本質(zhì)便是借助 Runtime 修改對象 isa 原本指向的類為新的派生類皱坛,并重寫期 setter 方法。換而言之豆巨,只有調(diào)用了對象的 setter 方法剩辟,才會觸發(fā)監(jiān)聽的方法。若 age 屬性聲明如下:
{
@public
NSInteger age;
}
并通過:
_v->age = 26;
這種形式賦值是不會觸發(fā)監(jiān)聽通知的往扔。
并且我們可以在不調(diào)用 setter 方法的時候手動觸發(fā)監(jiān)聽通知贩猎,那就是主動調(diào)用 willChangeValueForKey:
和 didChangeValueForKey:
兩個方法。
KVC
KVC (Key-Value Coding)萍膛,俗稱“鍵值編碼”吭服,可通過 key 訪問某個屬性。
基本使用
賦值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
keyPath 表示可對對象進行深層賦值:
現(xiàn)定義 Album 類蝗罗,描述專輯信息:
@interface Album : NSObject
@property(nonatomic, copy) NSString* genre;
@property(nonatomic, copy) NSString* artist;
@property(nonatomic, assign) NSInteger disc;
@end
在 Valenti 類中添加成員變量:
@property (nonatomic, strong) Album* album;
那么我們可通過深層路徑對 Album 的屬性進行監(jiān)聽:
[_v setValue:@"Pop" forKeyPath:@"album.genre"];
取值
- (nullable id)valueForKeyPath:(NSString*)keyPath;
- (nullable id)valueForKey:(NSString*)key;
NSKeyValueCoding 類別的其他方法
+ (BOOL)accessInstanceVariablesDirectly;
該方法默認返回 YES艇棕,如沒找到屬性的 setter 方法,會按照 _key串塑,_iskey沼琉,key,iskey 的順序搜索成員拟赊,若開發(fā)者重寫該方法刺桃,則系統(tǒng)不會遵循這個順序搜索。- (void)setNilValueForKey:(NSString *)key;
若 value 設置為 nil 會調(diào)用該方法。- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
給一組 key 得到與 key 對應的 value 的 [key : value] 形式的字典(和集合相關(guān)的 KVC API 都會導致性能的消耗)瑟慈。...
更多的其他方法在文章末尾有外鏈桃移。
原理
由上節(jié)得知賦值流程為:
我們將四種形式的 key 全都暴露出來驗證四個屬性的查找順序:
{
@public
NSInteger _age;
NSInteger _isAge;
NSInteger age;
NSInteger isAge;
}
在 setValue: forKey: 方法后打斷點看到 LLDB 環(huán)境下:
優(yōu)先給 _key 賦值。
注釋 _age 后打痈鸨獭:
優(yōu)先給 _isKey 賦值借杰。
注釋 _isAge 后打印:
優(yōu)先給 key 賦值进泼, 得到驗證蔗衡。
通過 KVO 章節(jié)闡述的過程中知道,這四個屬性都是無法觸發(fā)通知的乳绕,但是通過 KVC 對這種形式的屬性賦值绞惦,是能夠觸發(fā)通知的:
改變后的值 {
kind = 1;
new = 26;
old = 23;
}
這說明 KVC 的內(nèi)部實現(xiàn)能夠觸發(fā) key 改變的通知。
觸發(fā) KVO 的先決條件是 willChangeValueForKey: 以及 didChangeValueForKey: 那么我們在 Valenti 類重寫這兩個方法驗證:
@implementation Valenti
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey");
}
@end
運行:
willChangeValueForKey
改變后的值 {
kind = 1;
new = 26;
old = 23;
}
didChangeValueForKey
得到驗證洋措。
同理济蝉,KVC 取值的過程為:
若重寫了四個 getter 方法,調(diào)用順序同樣和 setter 方法一樣菠发。