KVO使用與原理分析

面試題目

  1. iOS用什么方式實現(xiàn)對一個對象的KVO退敦?(KVO的本質(zhì)是什么伍俘?)
  2. 如何手動觸發(fā)KVO邪锌?

上面兩道面試題目,都是在考察程序員對KVO的理解癌瘾。KVO對于一個iOS程序員來講并不陌生觅丰,在實際開發(fā)中我們或多或少都會使用到,在某些場景中妨退,會讓我們的開發(fā)變得更加簡單妇萄。那么你了解KVO的本質(zhì)嗎蜕企?它是如何實現(xiàn)的呢?

KVO簡介

在這里幫大家回憶一下KVO冠句,對于熟悉的同學向下翻閱轻掩。

概述

NSKeyValueObserving或者 KVO,是一個非正式協(xié)議懦底,它定義了對象之間觀察和通知狀態(tài)改變的通用機制的放典。KVO的中心思想其實是相當引人注意的。任意一個對象都可以訂閱以便被通知到其他對象狀態(tài)的改變基茵。這個過程大部分是內(nèi)建的奋构,自動的,透明的拱层。

KVO字面翻譯就是鍵值對監(jiān)聽弥臼,允許對象監(jiān)聽另一個對象特定屬性的改變,并在改變時接收到事件根灯。一般繼承自NSObject的對象都默認支持KVO径缅。

KVO可以監(jiān)聽單個屬性的變化,也可以監(jiān)聽集合對象(NSArrayNSSet)的變化烙肺。

基礎(chǔ)使用
  1. 添加觀察者:通過addObserver:forKeyPath:options:context:方法注冊觀察者纳猪,觀察者可以接收keyPath屬性的變化事件。
  2. 回調(diào):在觀察者中實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法桃笙,當keyPath屬性發(fā)生改變后氏堤,KVO會回調(diào)這個方法來通知觀察者。
  3. 當觀察者不需要監(jiān)聽時搏明,必須調(diào)用removeObserver:forKeyPath:方法將KVO移除鼠锈。
注冊觀察者
/*
observer:注冊KVO通知的對象。觀察者必須實現(xiàn) key-value observing 方法
observeValueForKeyPath:ofObject:change:context:星著。

keyPath:觀察者的屬性的 keypath购笆,相對于接受者,值不能是 nil虚循。

options: NSKeyValueObservingOptions 的組合同欠,它指定了觀察通知中包含了什
么,可以查看 "NSKeyValueObservingOptions"横缔。

context:在 observeValueForKeyPath:ofObject:change:context: 傳給observer
參數(shù)的隨機數(shù)據(jù)
*/
- (void)addObserver:(NSObject *)observer 
         forKeyPath:(NSString *)keyPath 
            options:(NSKeyValueObservingOptions)options 
            context:(nullable void *)context;

options代表NSKeyValueObservingOptions的位掩碼铺遂。

NSKeyValueObservingOptionNew = 0x01,  新值
NSKeyValueObservingOptionOld = 0x02,  舊值
NSKeyValueObservingOptionInitial = 0x04, 在注冊觀察者后,立即接收一次回調(diào)
NSKeyValueObservingOptionPrior = 0x08    會在變化前后收到兩次回調(diào)

context它可以被用作區(qū)分那些綁定同一個keypath的不同對象的觀察者剪廉。如何設置一個好的context?

static void *XXContext = &XXContext;

一個靜態(tài)變量存著它自己的指針娃循。在CocoaAsynSocket中也有類似的使用。

當然context還可以傳入任意類型的對象斗蒋,在接收消息回調(diào)的代碼中可以接收到這個對象捌斧,是KVO中的一種傳值方式。

回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

這些參數(shù)跟我們指定的 –addObserver:forKeyPath:options:context: 是一樣的泉沾,而change取決于哪個NSKeyValueObservingOptions選項被使用捞蚂。

更好的KeyPath

傳字符串做為keypath比直接使用屬性更糟糕,因為任何錯字或者拼寫錯誤都不會被編譯器察覺跷究,最終導致不能正常工作姓迅。 一個聰明的解決方案是使用 NSStringFromSelector和一個@selector字面值。

NSStringFromSelector(@selector(isFinished))

因為@selector檢查目標中的所有可用selector俊马,這并不能阻止所有的錯誤丁存,但它可用捕獲大部分-包括捕獲Xcode自動重構(gòu)帶來的改變。
那么在回調(diào)中可能是這樣子寫的:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if ([object isKindOfClass:[NSOperation class]]) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(isFinished))]) {

        }
    } else if (...) {
        // ...
    }
}
取消注冊

當一個觀察者完成了監(jiān)聽一個對象的改變柴我,需要調(diào)用removeObserver:forKeyPath:context:解寝。它經(jīng)常在observeValueForKeyPath:ofObject:change:context:,或者dealloc中被調(diào)用艘儒。

注意
  1. 在調(diào)用addObserver方法后聋伦,KVO并不會對觀察者進行強引用,所以需要注意觀察者的生命周期界睁。
  2. KVOaddObserverremoveObserver必須成對出現(xiàn)觉增,如果重復removeObserver則會導致Crash,如果在觀察者釋放前翻斟,忘記removeObserver逾礁,則會在再次接收到KVO回調(diào)時Crash
  3. 觀察者必須實現(xiàn)key-value observing方法
    observeValueForKeyPath:ofObject:change:context:,否則會Crash访惜。
  4. KVO也有對應集合的實現(xiàn)敞斋,包括我們常用的NSArray,可以去KVO的頭文件中查看疾牲,有對應的鍵值參數(shù)等植捎。

蘋果官方推薦的方式是,在init的時候進行addObserver阳柔,在deallocremoveObserver焰枢,這樣可以保證addremove是成對出現(xiàn)的,是一種比較理想的使用方式舌剂。

KVO本質(zhì)

我們可以翻看蘋果文檔

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

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. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

KVO是通過基于runtime技術(shù)實現(xiàn)的济锄。當某個對象被觀察時,系統(tǒng)的runtime運行時會動態(tài)的實現(xiàn)一個基于該類的一個中間類(派生類)霍转,在中間類中實現(xiàn)被觀察基類屬性的setter方法荐绝,中間類在重寫的setter方法中實現(xiàn)真正的通知機制。

同時派生類還重寫了- (Class)class;方法以“欺騙”外部調(diào)用者它就是起初的那個類避消。然后系統(tǒng)將被觀察對象的isa指針指向這個新誕生的派生類低滩,因此這個對象就成為該派生類的對象了召夹,因而在該對象上對被觀察屬性setter的調(diào)用就會調(diào)用重寫的 setter,從而激活鍵值通知機制恕沫。此外监憎,派生類還重寫了dealloc方法來釋放資源。

通過代碼驗證一波:

/* 被觀察對象模型 */
@interface KVOObject : NSObject
@property (nonatomic, copy) NSString *objName;
@end
/* 在控制器的viewDidLoad中 */
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.object_1 = [[KVOObject alloc] init];
    self.object_1.objName = @"object_1---前";

    [self isaPointerOfObject:self.object_1];

    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | 
    NSKeyValueObservingOptionOld;
    [self.object_1 addObserver:self forKeyPath:@"objName" options:options
     context:kObserverContext];
    
    self.object_1.objName = @"object_1---后";

    [self isaPointerOfObject:self.object_1];
}
/* 打印被觀察對象所屬類... */
- (void)isaPointerOfObject:(KVOObject *)obj{
    
    Class objectMethodClass = [obj class];
    Class objectRuntimeClass = object_getClass(obj);
    Class superClass = class_getSuperclass(objectRuntimeClass);
    
    NSLog(@"%@-objectMethodClass:%@",obj.objName,objectMethodClass);
    NSLog(@"%@-objectRuntimeClass:%@",obj.objName,objectRuntimeClass);
    NSLog(@"%@-superClass:%@",obj.objName,superClass);
}
控制臺打印_1

通過控制臺打印的內(nèi)容對比婶溯,對象被KVO后鲸阔,其真正類型變?yōu)榱?code>NSKVONotifying_KVOObject類,已經(jīng)不是之前的類,其實是將被觀察對象的isa從指向的KVOObject的class迄委,改為系統(tǒng)“偷偷”幫我們創(chuàng)建的NSKVONotifying_KVOObject類的class褐筛。新類是原類的子類,命名規(guī)則是NSKVONotifying_xxx的格式叙身。

同時我們發(fā)現(xiàn)- (Class)class;在注冊觀察者前后渔扎,結(jié)果卻是一致的。這其實是蘋果對我們的一種“欺騙”曲梗,在中間類中重寫了- (Class)class方法赞警。

/* 打印查看方法*/
- (void)methodListOfObject:(KVOObject *)obj
{
    Class objectRuntimeClass = object_getClass(obj);
    
    unsigned int count;
    Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
    for (NSInteger i = 0; i < count; i++) {
        Method method = methodList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"%@-method Name = %@\n",obj.objName,methodName);
    }
    free(methodList);
}
控制臺打印_2

在新創(chuàng)建的中間類中,重寫了setter:,dealloc,class方法虏两,添加新的方法_isKVOA愧旦。dealloc推測在添加觀察者時,增加了一些依賴定罢,需要在對象釋放時銷毀笤虫。_isKVOA方法,這個方法可以當做使用了KVO的一個標記祖凫,系統(tǒng)可能也是這么用的琼蚯。如果我們想判斷當前類是否是KVO動態(tài)生成的類,就可以從方法列表中搜索這個方法惠况。

那么系統(tǒng)重寫了setter:后是如何實現(xiàn)通知的呢遭庶?由于蘋果API這部分沒有開源,但是通過函數(shù)調(diào)用棧稠屠,我們推測一二峦睡。

/* 在KVOObject的.m文件中...*/
@implementation KVOObject

// 是否允許通知..
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    NSLog(@"automaticallyNotifiesObserversForKey:");
    return YES;
}

// 重寫setter
- (void)setObjName:(NSString *)objName
{
    NSLog(@"setter方法,屬性發(fā)生修改 - 前");
    _objName = objName;
    NSLog(@"setter方法,屬性發(fā)生修改 - 后");
}

// 重寫willChangeValueForKey
- (void)willChangeValueForKey:(NSString *)key
{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}

// 重寫didChangeValueForKey
- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey - 前");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - 后");
}

@end

控制臺打印結(jié)果:

控制臺_3.png

函數(shù)調(diào)用棧:


調(diào)用棧_4.png
調(diào)用棧_5.png

控制臺打印結(jié)果和函數(shù)調(diào)用棧推測:重寫的setter:內(nèi)部調(diào)用了_NSSetObjectValueAndNotify的方法(這是一個系列的方法,還有_NSSetIntValueAndNotify权埠、_NSSetDoubleValueAndNotify等)榨了。

在這個明顯C風格命名的方法內(nèi)部,

  1. 先調(diào)用了willChangeValueForKey:
  2. 然后調(diào)用父類的-setter:方法攘蔽,對值進行修改
  3. 修改完后再調(diào)用didChangeValueForKey:方法
  4. didChangeValueForKey:的內(nèi)部最終調(diào)用了NSKeyValueNotifyObserver方法龙屉,通知屬性的觀察者,觀察者收到了值修改的信息
  5. didChangeValueForKey:調(diào)用完畢

至此满俗,在能力范圍內(nèi)的對KVO內(nèi)部的分析已經(jīng)完畢转捕。相信看到這里的同學對 iOS用什么方式實現(xiàn)對一個對象的KVO作岖?(KVO的本質(zhì)是什么?)已經(jīng)可以回答出來了瓜富。

如何手動觸發(fā)KVO鳍咱?怎么弄...
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key

控制是否自動發(fā)送通知降盹,如果返回NO与柑,KVO無法自動運作,需手動觸發(fā)蓄坏。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"objName"]) {
        return NO;
    }
    return YES;
}

- (void)setObjName:(NSString *)objName
{
    if (<#條件判斷##>){
        [self willChangeValueForKey:@"objName"];
    }
    _objName = objName;
    if (<#條件判斷##>) {
        [self didChangeValueForKey:@"objName"];
    }
}

比如我們想對objName屬性手動出發(fā)KVO,就在+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key中對objName返回NO,并在屬性-setter方法中价捧,手動調(diào)用willChangeValueForKeydidChangeValueForKey必須兩者都調(diào)用),這樣就可以實現(xiàn)手動來出發(fā)KVO了涡戳。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末结蟋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子渔彰,更是在濱河造成了極大的恐慌嵌屎,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恍涂,死亡現(xiàn)場離奇詭異宝惰,居然都是意外死亡,警方通過查閱死者的電腦和手機再沧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門尼夺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人炒瘸,你說我怎么就攤上這事淤堵。” “怎么了顷扩?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵拐邪,是天一觀的道長。 經(jīng)常有香客問我隘截,道長扎阶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任技俐,我火速辦了婚禮乘陪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雕擂。我一直安慰自己啡邑,他們只是感情好,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布井赌。 她就那樣靜靜地躺著谤逼,像睡著了一般贵扰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上流部,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天戚绕,我揣著相機與錄音,去河邊找鬼枝冀。 笑死舞丛,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的果漾。 我是一名探鬼主播球切,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绒障!你這毒婦竟也來了吨凑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤户辱,失蹤者是張志新(化名)和其女友劉穎鸵钝,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庐镐,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡恩商,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了焚鹊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片痕届。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖末患,靈堂內(nèi)的尸體忽然破棺而出研叫,到底是詐尸還是另有隱情,我是刑警寧澤璧针,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布嚷炉,位于F島的核電站,受9級特大地震影響探橱,放射性物質(zhì)發(fā)生泄漏申屹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一隧膏、第九天 我趴在偏房一處隱蔽的房頂上張望哗讥。 院中可真熱鬧,春花似錦胞枕、人聲如沸杆煞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽决乎。三九已至队询,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間构诚,已是汗流浹背蚌斩。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留范嘱,地道東北人送膳。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像彤侍,于是被迫代替她去往敵國和親肠缨。 傳聞我的和親對象是個殘疾皇子逆趋,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉盏阶,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,679評論 0 9
  • 該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯[http://www.reibang.com/u/2de707c93d...
    劉小壯閱讀 48,265評論 35 227
  • [深入淺出Cocoa]詳解鍵值觀察(KVO)及其實現(xiàn)機理羅朝輝 (http://www.cppblog.com/k...
    Crazy2015閱讀 678評論 0 1
  • KVC 什么是 KVC KVC 是 Key-Value-Coding 的簡稱闻书。 KVC 是一種可以直接通過字符串的...
    LeeJay閱讀 2,207評論 6 41
  • “该澹”字為我國最古老的文字之一,從“钙敲迹”字演變史上記載砰盐,“福”字主要經(jīng)歷了甲骨文向金文坑律,再向小篆的演變過程:...
    枯枝杜鵑閱讀 37,242評論 0 4