iOS - 有關(guān) KVO 和 KVC

image

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 指針析校,如圖:

image

發(fā)現(xiàn) v 的 isa 指針指向 NSKVONotifying_Valenti,同理打印 v2 的 isa 指針铜涉,結(jié)果為:

image

v2 的 isa 指針指向的是 Valenti智玻。

我們在對象 v 添加監(jiān)聽之前和之后分別打印 object_getClass() 返回結(jié)果:

image

運行結(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é)果為:


image.png

發(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é)果如下:

image

原來 NSKVONotifying_Valenti 是 Valenti 的子類。

image

那么肝谭,動態(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)系為:

image

最后在 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é)得知賦值流程為:


image

我們將四種形式的 key 全都暴露出來驗證四個屬性的查找順序:

{
    @public
    NSInteger _age;
    NSInteger _isAge;
    NSInteger age;
    NSInteger isAge;
}

在 setValue: forKey: 方法后打斷點看到 LLDB 環(huán)境下:


image

優(yōu)先給 _key 賦值。
注釋 _age 后打痈鸨獭:


image

優(yōu)先給 _isKey 賦值借杰。
注釋 _isAge 后打印:
image

優(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 取值的過程為:


image

若重寫了四個 getter 方法,調(diào)用順序同樣和 setter 方法一樣菠发。

參考

Mike Ash 對 KVO 的本質(zhì)探究
iOS KVO和KVC 詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末王滤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子滓鸠,更是在濱河造成了極大的恐慌雁乡,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件糜俗,死亡現(xiàn)場離奇詭異踱稍,居然都是意外死亡,警方通過查閱死者的電腦和手機悠抹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門寞射,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锌钮,你說我怎么就攤上這事桥温。” “怎么了梁丘?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵侵浸,是天一觀的道長。 經(jīng)常有香客問我氛谜,道長掏觉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任值漫,我火速辦了婚禮澳腹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己酱塔,他們只是感情好沥邻,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著羊娃,像睡著了一般唐全。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蕊玷,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天邮利,我揣著相機與錄音,去河邊找鬼垃帅。 笑死延届,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的贸诚。 我是一名探鬼主播祷愉,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼赦颇!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赴涵,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤媒怯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后髓窜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扇苞,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年寄纵,在試婚紗的時候發(fā)現(xiàn)自己被綠了鳖敷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡程拭,死狀恐怖定踱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情恃鞋,我是刑警寧澤崖媚,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站恤浪,受9級特大地震影響畅哑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜水由,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一荠呐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦泥张、人聲如沸呵恢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瑰剃。三九已至,卻和暖如春筝野,著一層夾襖步出監(jiān)牢的瞬間晌姚,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工歇竟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留挥唠,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓焕议,卻偏偏與公主長得像宝磨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子盅安,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354