iOS - KVO 底層詳解及與 KVC 的關(guān)系

封面

一、KVO 簡介

KVO(Key-Value Observing)是iOS提供的一種監(jiān)聽屬性變化的機制缭召。

二重斑、使用場景

基本使用:

  1. 添加觀察者
    任意定義一個包含了屬性的類:
  @interface KVO : NSObject
  @property (nonatomic, assign) NSUInteger count;
  @property (nonatomic, copy)   NSString   *name;
  @end

添加一個對上述類實例對象的屬性值監(jiān)聽者:

  KVO  *kvoObj = [KVO new];
  [kvoObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

上述代碼對kvoObj的name屬性進(jìn)行了監(jiān)聽银酗,其中監(jiān)聽的策略是NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld堰酿,表示屬性值改變時通知內(nèi)容里包含新的值和老的值;

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01, //指明接受通知方法參數(shù)中的change字典中應(yīng)該包含改變后的新值昭卓。
NSKeyValueObservingOptionOld = 0x02, //指明接受通知方法參數(shù)中的change字典中應(yīng)該包含改變前的舊值愤钾。
NSKeyValueObservingOptionInitial = 0x04, //當(dāng)指定了這個選項時,在addObserver:forKeyPath:options:context:消息被發(fā)出去后候醒,甚至不用等待這個消息返回能颁,監(jiān)聽者對象會馬上收到一個通知
NSKeyValueObservingOptionPrior = 0x08 //當(dāng)指定了這個選項時,在被監(jiān)聽的屬性被改變前火焰,監(jiān)聽者對象就會收到一個通知
}

另外劲装,context這里直接用nil,其實這個參數(shù)可以用來傳值或者使用靜態(tài)變量來標(biāo)志一個指定的通知昌简。

  1. 添加觀察者通知響應(yīng)函數(shù)
    需要重寫非正式協(xié)議NSKeyValueObserving的下述方法以接收屬性值改變時發(fā)出的通知:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}

這里只簡單的打印出change里的信息;

  1. 屬性改變時通知觀察者
kvoObj.name = @"newName";

運行結(jié)果:

屬性改變通知

打印出了change字典對象里的內(nèi)容绒怨,new和old分別對應(yīng)NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld選項纯赎;
其中,kind = 1表示改變的類型為設(shè)置類型南蹂,具體代碼對應(yīng)的類型如下:

enum {
 NSKeyValueChangeSetting = 1,
 NSKeyValueChangeInsertion = 2, //針對集合類型屬性犬金,表示插入元素到該集合對象
 NSKeyValueChangeRemoval = 3, //同上,表示移除元素從該集合對象
 NSKeyValueChangeReplacement = 4 //同上六剥,表示從該集合對象屬相總替換元素
};
typedef NSUInteger NSKeyValueChange;

需要注意晚顷,添加監(jiān)聽的方法addObserver:forKeyPath:options:context:并不會對監(jiān)聽和被監(jiān)聽的對象以及context做強引用,你必須自己保證他們在監(jiān)聽過程中不被釋放疗疟。

  1. 移除觀察者
    當(dāng)不再需要監(jiān)聽該屬性的時候该默,或者觀察者需要被釋放前,需要從被觀察者隊列中移除策彤,否則被觀察者繼續(xù)發(fā)送通知則會導(dǎo)致野指針程序崩潰栓袖,具體實現(xiàn)如下:
- (void)dealloc {
    [kvoObj removeObserver:self forKeyPath:@"name"];
}

三匣摘、自己實現(xiàn)KVO

蘋果官方文檔:

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

大概意思就是說利用了isa_swizzling技術(shù),大家都知道swizzling是一種OC級別的Hook技術(shù)裹刮,所以isa_swizzling就是一種isa Hook技術(shù)音榜,在一個支持KVO的對象被添加了觀察者,系統(tǒng)會為其生成一個子類捧弃,重寫了setXXX方法(XXX為被監(jiān)聽的屬性名)赠叼,并將該實例的Isa指針指向了新的這個子類(class),這樣對被觀察者進(jìn)行屬性賦值的時候調(diào)用的是重寫后的setXXX方法,而setXXX方法內(nèi)部添加了通知機制违霞;
那么我們自己手動來實現(xiàn)一個簡單的KVO:

  • 3.1 自己實現(xiàn)一個添加觀察者的方法:
//
//  SUKVO.h
//  DreamOneByOne
//
//  Created by He on 2017/7/16.
//  Copyright ? 2017年 Sevenuncle. All rights reserved.
//

#import <Foundation/Foundation.h>

@protocol SUKVODelegate <NSObject>

-(void)su_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

@end

@interface SUKVO : NSObject

@property (copy, nonatomic) NSString *desc;

- (void)su_addObserver:(NSObject *)observer forKeyPath:(NSString *)path options:(NSKeyValueObservingOptions)options context:(void *)context;

@end

模仿NSKeyValueObserving協(xié)議(分類)定義添加觀察者的方法以及一個代理用于接收通知嘴办。下面是其內(nèi)部實現(xiàn):

// SUKVO.m
#import "SUKVO.h"
#import "SUKVO_SubClass.h"
#import <objc/message.h>
@implementation SUKVO

- (void)su_addObserver:(NSObject *)observer forKeyPath:(NSString *)path options:(NSKeyValueObservingOptions)options context:(void *)context {
    
    //動態(tài)添加觀察者對象
    objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //動態(tài)改變被觀察者isa指針,使訪問到改變后的setXXX方法
    object_setClass(self, [SUKVO_SubClass class]);
}
@end

當(dāng)添加入一個觀察者時葛家,利用了runtime動態(tài)添加屬性接口將該觀察者加入該被觀察實例對象上(實際上需要維護(hù)一個隊列户辞,用于記錄所有觀察該屬性的觀察者,這里為了簡單)癞谒,用于后續(xù)監(jiān)聽屬性改變時給這個觀察者發(fā)送消息底燎;
然后,就是同樣利用runtime改變該被觀察者實例對象的isa指針(class)弹砚,這樣后續(xù)發(fā)送消息給被觀察者均是往SUKVO_SubClass定義的方法里找(系統(tǒng)實現(xiàn)的子類為NSKVONotifying_原有類)双仍;

  • 3.2 接下來看一下SUKVO_SubClass子類的實現(xiàn):
#import "SUKVO_SubClass.h"
#import <objc/message.h>

@implementation SUKVO_SubClass

- (void)setDesc:(NSString *)desc {
    [self willChangeValueForKey:@"desc"];  //改變之前通知
    [super setDesc:desc];
    [self didChangeValueForKey:@"desc"]; //改變之后通知

    //通知值改變,這里為了圖方便桌吃,簡單的直接發(fā)送改變通知朱沃,實際上系統(tǒng)的實現(xiàn)利用了消息通知機制
    id observer = objc_getAssociatedObject(self, "observer");
    if([observer respondsToSelector:@selector(su_observeValueForKeyPath:ofObject:change:context:)]) {
        [observer su_observeValueForKeyPath:@"desc" ofObject:self change:nil context:nil];
    }
}
@end

這樣就實現(xiàn)了簡單的KVO,不過系統(tǒng)為了滿足可以添加多個觀察者監(jiān)聽同一個屬性的需求茅诱,不能像上述實現(xiàn)的這么簡單逗物,需要一個一個字典加隊列,這樣每一個屬性對應(yīng)一個觀察者隊列瑟俭,然后由內(nèi)部一個通知中心統(tǒng)一給觀察者發(fā)送通知翎卓;

四、與 KVC 的關(guān)系

關(guān)于KVO和KVC之間是否有聯(lián)系摆寄,在網(wǎng)絡(luò)上搜索了一通失暴,也沒個定論,不過大眾普遍認(rèn)為KVO和KVC通常是有聯(lián)系的微饥;但是逗扒,當(dāng)了解了KVO的實現(xiàn)機制后,如上面自己實現(xiàn)KVO中欠橘,發(fā)現(xiàn)并未用到KVC矩肩,所以部分人開始懷疑KVO真的是基于KVC實現(xiàn)的嗎?
那么試想一個問題:

對于一個包含只讀(readonly)屬性的變量简软,為什么也能通過setVaule:forKey進(jìn)行賦值蛮拔?因為對于默認(rèn)readonly屬性述暂,系統(tǒng)是不會生成set的屬性賦值方法的?那根據(jù)KVO的原理建炫,是無法進(jìn)行鍵值改變的監(jiān)聽的畦韭?

為了驗證上面這個問題,我們需要確認(rèn)兩個事情:

  1. 對于一個readonly屬性并且同時對該屬性進(jìn)行了觀察者監(jiān)聽肛跌,是否有setXXX方法艺配?
  2. 是否能夠使用KVC對readonly屬性賦值?

如果上述不包含setXXX方法并且能夠使用KVC對只讀屬性賦值衍慎,就說明KVC內(nèi)部包含了對KVO的支持转唉!

  • 下面開始驗證第一個問題:

下面的KVCObject類包含了一個只讀屬性readonly和一個讀寫的屬性,

@interface KVCObject : NSObject

@property (nonatomic, assign)           NSUInteger count;
@property (nonatomic, copy, readonly)   NSString   *name;
@property (nonatomic, copy)             NSString   *location;

@end

同時對一個KVCObject實例對象添加了對name屬性進(jìn)行監(jiān)聽的觀察者:

kvcObj = [KVCObject new];
[kvcObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[kvcObj addObserver:self forKeyPath:@"location" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

打印出此時kvcObj所在的類包含的實例方法:

KVCObject *tmpObj = kvcObj;
    Class currentClass = object_getClass(tmpObj);
    unsigned int methodCount;
    Method *methodList = class_copyMethodList(currentClass, &methodCount);
    int i = 0;
    for (; i < methodCount; i++) {
        NSLog(@"%@ - %@", [NSString stringWithCString:class_getName(currentClass) encoding:NSUTF8StringEncoding], [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding]);
    }
    if( [tmpObj respondsToSelector:@selector(setName:)]) {
        NSLog(@"name");
    }
    if( [tmpObj respondsToSelector:@selector(setLocation:)]) {
        NSLog(@"location");
    }

上面的代碼打印出了一個對象添加了監(jiān)聽之后,生成的新的子類包含的實例方法列表:

NSKVONotifying_KVCObject類

可以看出稳捆,讀寫屬性NSString * location生成了對應(yīng)的setXXX方法赠法,而只讀屬性name沒有生成對應(yīng)的setXXX方法,所以第一個問題得到驗證乔夯。

  • 驗證第二個問題:此時通過KVC改變只讀屬性的值砖织,能夠得到KVO值改變通知?
 [kvcObj setValue:@"newName" forKey:@"name"];

監(jiān)聽回調(diào)方法:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}

輸出結(jié)果:


KVC修改只讀屬性

可以看出末荐,只讀屬性name在沒有setName方法的情況下侧纯,通過KVC改變值得方式也得到了KVO通知,所以可以下結(jié)論甲脏,KVC內(nèi)部的實現(xiàn)機制支持了KVO眶熬,KVO是依賴KVC的,并不像部分人懷疑的KVC和KVO之間毫無聯(lián)系块请。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末娜氏,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子墩新,更是在濱河造成了極大的恐慌牍白,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,185評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抖棘,死亡現(xiàn)場離奇詭異,居然都是意外死亡狸涌,警方通過查閱死者的電腦和手機切省,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帕胆,“玉大人朝捆,你說我怎么就攤上這事±帘” “怎么了芙盘?”我有些...
    開封第一講書人閱讀 157,684評論 0 348
  • 文/不壞的土叔 我叫張陵驯用,是天一觀的道長。 經(jīng)常有香客問我儒老,道長蝴乔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,564評論 1 284
  • 正文 為了忘掉前任驮樊,我火速辦了婚禮薇正,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘囚衔。我一直安慰自己挖腰,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,681評論 6 386
  • 文/花漫 我一把揭開白布练湿。 她就那樣靜靜地躺著猴仑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肥哎。 梳的紋絲不亂的頭發(fā)上辽俗,一...
    開封第一講書人閱讀 49,874評論 1 290
  • 那天,我揣著相機與錄音贤姆,去河邊找鬼榆苞。 笑死,一個胖子當(dāng)著我的面吹牛霞捡,可吹牛的內(nèi)容都是我干的坐漏。 我是一名探鬼主播,決...
    沈念sama閱讀 39,025評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼碧信,長吁一口氣:“原來是場噩夢啊……” “哼赊琳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起砰碴,我...
    開封第一講書人閱讀 37,761評論 0 268
  • 序言:老撾萬榮一對情侶失蹤躏筏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后呈枉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體趁尼,經(jīng)...
    沈念sama閱讀 44,217評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,545評論 2 327
  • 正文 我和宋清朗相戀三年猖辫,在試婚紗的時候發(fā)現(xiàn)自己被綠了酥泞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,694評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡啃憎,死狀恐怖芝囤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤悯姊,帶...
    沈念sama閱讀 34,351評論 4 332
  • 正文 年R本政府宣布羡藐,位于F島的核電站,受9級特大地震影響悯许,放射性物質(zhì)發(fā)生泄漏仆嗦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,988評論 3 315
  • 文/蒙蒙 一岸晦、第九天 我趴在偏房一處隱蔽的房頂上張望欧啤。 院中可真熱鬧,春花似錦启上、人聲如沸邢隧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,778評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽倒慧。三九已至,卻和暖如春包券,著一層夾襖步出監(jiān)牢的瞬間纫谅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,007評論 1 266
  • 我被黑心中介騙來泰國打工溅固, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留付秕,地道東北人。 一個月前我還...
    沈念sama閱讀 46,427評論 2 360
  • 正文 我出身青樓侍郭,卻偏偏與公主長得像询吴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子亮元,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,580評論 2 349

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