KVO實現原理及自己實現KVO

前言

Key-Value-Observer,它來源于觀察者模式, 其基本思想(copy于某度)是一個目標對象管理所有依賴于它的觀察者對象,并在它自身的狀態改變時主動通知觀察者對象。這個主動通知通常是通過調用各觀察者對象所提供的接口方法來實現的。觀察者模式較完美地將目標對象與觀察者對象解耦。

本質

  • KVO 是 Objective-C 對觀察者設計模式的一種實現,另外一種是:通知機制(notification)
  • KVO提供一種機制,指定一個被觀察對象(例如A類),當對象某個屬性(例如A中的字符串name)發生更改時,對象會獲得通知,并作出相應處理

在MVC設計架構下的項目,KVO機制很適合實現mode模型和controller之間的通訊。
例如:代碼中,在模型類A創建屬性數據,在控制器中創建觀察者,一旦屬性數據發生改變就收到觀察者收到通知,通過KVO再在控制器使用回調方法處理實現視圖B的更新;(本文中的應用就是這樣的例子.)

實現原理

KVO在Apple中的API文檔如下:
Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …
KVO 的實現依賴于 Objective-C 強大的 Runtime【 ,從以上Apple 的文檔可以看出蘋果對于KVO機制的實現是一筆帶過,而具體的細節沒有過多的描述,但是我們可以通過Runtime的所提供的方法去探索關于KVO機制的底層實現原理.

基本的原理:

當觀察某對象A時,KVO機制動態創建一個對象A當前類的子類,并為這個新的子類重寫了被觀察屬性keyPath的setter 方法。setter 方法隨后負責通知觀察對象屬性的改變狀況。

深入剖析:

Apple 使用了 isa 混寫(isa-swizzling)來實現 KVO 。當觀察對象A時,KVO機制動態創建一個新的名為: NSKVONotifying_A的新類,該類繼承自對象A的本類,且KVO為NSKVONotifying_A重寫觀察屬性的setter 方法,setter 方法會負責在調用原 setter 方法之前和之后,通知所有觀察對象屬性值的更改情況。

  • NSKVONotifying_A類剖析:在這個過程,被觀察對象的 isa 指針從指向原來的A類,被KVO機制修改為指向系統新創建的子類 NSKVONotifying_A類,來實現當前類屬性值改變的監聽;
    所以當我們從應用層面上看來,完全沒有意識到有新的類出現,這是系統“隱瞞”了對KVO的底層實現過程,讓我們誤以為還是原來的類。但是此時如果我們創建一個新的名為“NSKVONotifying_A”的類(),就會發現系統運行到注冊KVO的那段代碼時程序就崩潰,因為系統在注冊監聽的時候動態創建了名為NSKVONotifying_A的中間類,并指向這個中間類了。
    因而在該對象上對 setter 的調用就會調用已重寫的 setter,從而激活鍵值通知機制。

  • 子類setter方法剖析:KVO的鍵值觀察通知依賴于 NSObject 的兩個方法:willChangeValueForKey:和 didChangevlueForKey:,在存取數值的前后分別調用2個方法:
    被觀察屬性發生改變之前,willChangeValueForKey:被調用,通知系統該 keyPath 的屬性值即將變更;當改變發生后, didChangeValueForKey: 被調用,通知系統該 keyPath 的屬性值已經變更;
    之后observeValueForKey:ofObject:change:context: 也會被調用。且重寫觀察屬性的setter 方法這種繼承方式的注入是在運行時而不是編譯時實現的。
    KVO為子類的觀察者屬性重寫調用存取方法的工作原理在代碼中相當于:

-(void)setName:(NSString *)newName
{
    [self willChangeValueForKey:@"name"];    //KVO在調用存取方法之前總調用
    [super setValue:newName forKey:@"name"]; //調用父類的存取方法
    [self didChangeValueForKey:@"name"];     //KVO在調用存取方法之后總調用
}

示例驗證

//Person類
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@end

//controller
Person *per = [[Person alloc]init];
//斷點1
[per addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
//斷點2
per.name = @"小明";
[per removeObserver:self forKeyPath:@"name"];
//斷點3

運行項目,

  • 斷點1位置:

    可以看到isa指向Person類,我們也可以使用lldb命令查看:
(lldb) po [per class]
Person
(lldb) po object_getClass(per)
Person
(lldb) 
  • 斷點2位置:
(lldb) po [per class]
Person
(lldb) po object_getClass(per)
NSKVONotifying_Person
(lldb) 
  • 斷點3位置:
(lldb) po [per class]
Person
(lldb) po object_getClass(per)
Person
(lldb)

上面的結果說明,在per對象被觀察時,framework使用runtime動態創建了一個Person類的子類NSKVONotifying_Person,而且為了隱藏這個行為,NSKVONotifying_Person重寫了- class方法返回之前的類,就好像什么也沒發生過一樣。但是使用object_getClass()時就暴露了,因為這個方法返回的是這個對象的isa指針,這個指針指向的一定是個這個對象的類對象
然后來偷窺一下這個動態類實現的方法,這里請出一個NSObject的擴展NSObject+DLIntrospection,它封裝了打印一個類的方法、屬性、協議等常用調試方法,一目了然。

@interface NSObject (DLIntrospection) 
+ (NSArray *)classes; 
+ (NSArray *)properties; 
+ (NSArray *)instanceVariables; 
+ (NSArray *)classMethods; 
+ (NSArray *)instanceMethods; 
 
+ (NSArray *)protocols; 
+ (NSDictionary *)descriptionForProtocol:(Protocol *)proto; 
 
+ (NSString *)parentClassHierarchy; 
@end

然后繼續在剛才的斷點處調試:

// 斷點1 
(lldb) po [object_getClass(per) instanceMethods] 
<__NSArrayI 0x8e9aa00>( 
- (void)setName:(id)arg0 , 
- (void).cxx_destruct, 
- (id)name 
) 
// 斷點 2 
(lldb) po [object_getClass(per) instanceMethods] 
<__NSArrayI 0x8d55870>( 
- (void)setName:(id)arg0 , 
- (class)class, 
- (void)dealloc, 
- (BOOL)_isKVOA 
) 
// 斷點 3 
(lldb) po [object_getClass(per) instanceMethods] 
<__NSArrayI 0x8e9cff0>( 
- (void)setName:(id)arg0 , 
- (void).cxx_destruct, 
- (id)name 
) 

大概就是說arc下這個方法在所有dealloc調用完成后負責釋放所有的變量,當然這個和KVO沒啥關系了,回到正題。
從上面斷點2的打印可以看出,動態類重寫了4個方法:

  • - setName:最主要的重寫方法,set值時調用通知函數
  • - class隱藏自己必備啊,返回原來類的class
  • - dealloc做清理犯罪現場工作
  • - _isKVOA這就是內部使用的標示了,判斷這個類有沒被KVO動態生成子類

接下來驗證一下KVO重寫set方法后是否調用了- willChangeValueForKey:和- didChangeValueForKey:
最直接的驗證方法就是在Person類中重寫這兩個方法:

@implementation Person 
- (void)willChangeValueForKey:(NSString *)key { 
    NSLog(@"%@", NSStringFromSelector(_cmd)); 
    [super willChangeValueForKey:key]; 
} 
- (void)didChangeValueForKey:(NSString *)key { 
    NSLog(@"%@", NSStringFromSelector(_cmd)); 
    [super didChangeValueForKey:key]; 
} 
@end 

自己代碼實現KVO

由于系統是自動實現的派生類NSKVONotifying_Person, 這兒我們自己手動創建一個派生類ALINKVONotifying_Person, 集成自Person. 同時給NSObject創建一個分類, 讓每一個對象都擁有我們自定義的KVO特性.

//NSObject+KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (KVO)
- (void)czc_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end

//NSObject+KVO.m
#import "NSObject+KVO.h"
#import "ALINKVONotifying_Person.h"
#import <objc/message.h>
NSString *const ObserverKey = @"ObserverKey";

@implementation NSObject (KVO)
- (void)czc_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
    
    // 把觀察者保存到當前對象
    objc_setAssociatedObject(self, (__bridge const void *)(ObserverKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 修改對象isa指針
    object_setClass(self, [ALINKVONotifying_Person class]);
}
@end

//ALINKVONotifying_Person.m
#import "ALINKVONotifying_Person.h"
#import <objc/runtime.h>
extern NSString *const ObserverKey;
@implementation ALINKVONotifying_Person
- (void)setName:(NSString *)name{
    NSString *oldName = self.name;
     [super setName:name];
    // 獲取觀察者
    id obsetver = objc_getAssociatedObject(self, ObserverKey);
    NSDictionary<NSKeyValueChangeKey,id> *changeDict = oldName ? @{NSKeyValueChangeNewKey : name, NSKeyValueChangeOldKey : oldName} : @{NSKeyValueChangeNewKey : name};
    [obsetver observeValueForKeyPath:@"name" ofObject:self change:changeDict context:nil];
}
@end

此時我們調用自己定義的監聽方法, 效果和系統的也是一樣的

[per czc_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容

  • 一、概述 KVO,即:Key-Value Observing,它提供一種機制,當指定的對象的屬性被修改后,則其觀察...
    DeerRun閱讀 10,106評論 11 33
  • KVC 什么是 KVC KVC 是 Key-Value-Coding 的簡稱。 KVC 是一種可以直接通過字符串的...
    LeeJay閱讀 2,217評論 6 41
  • 本篇會對KVO的實現進行探究,不涉及太多KVO的使用方法,但是會有一些使用時的思考。 一、使用上的疑問 1.key...
    奮拓達閱讀 523評論 0 2
  • 上半年有段時間做了一個項目,項目中聊天界面用到了音頻播放,涉及到進度條,當時做android時候處理的不太好,由于...
    DaZenD閱讀 3,034評論 0 26
  • 作者:wangzz原文地址:http://blog.csdn.net/wzzvictory/article/det...
    反調唱唱閱讀 1,124評論 0 5