iOS KVC和KVO介紹

概述

KVC:鍵值編碼昭伸,使用字符串的方式管理對象的成員、屬性
KVO:鍵值監(jiān)聽,一種觀察者模式顶籽,監(jiān)聽屬性的改變抑胎,可實現(xiàn)UI和數(shù)據(jù)模型的分離,基于KVC像捶。

鍵值編碼KVC(NSKeyValueCoding)

作用:動態(tài)管理對象的成員變量讀寫操作。

KVC的操作方法有是由 NSKeyValueCoding 協(xié)議提供,NSObject 實現(xiàn)了這個協(xié)議沉眶,這意味著OC中幾乎所有的對象都支持KVC操作。

使用方式:

  • 簡單路徑
設(shè)值:[obj setValue:值 forKey:變量名]
取值:[obj valueForKey:變量名]
  • 復(fù)合路徑
設(shè)值:[obj setValue:值 forKeyPath:變量路徑]
取值:[obj valueForKeyPath:變量路徑]

示例:

我們定義一個Person類杉适,并聲明一個name的屬性

Person.h文件

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

我們在主程序中使用KVC來控制name的取谎倔、設(shè)值操作

Person *person = [Person new];
[person setValue:@"LOLITA" forKey:@"name"];
NSLog(@"-->%@",[person valueForKey:@"name"]); 

KVC不僅可以設(shè)置person的屬性,person的成員變量也可以操作猿推,不管是公有還是私有片习。

我們給Person類新增成員變量

Person.h

@interface Person : NSObject
{
    @private
    NSString *_sex;
    @public
    CGFloat _height;
}
@property (copy ,nonatomic) NSString *name;
@end

主程序中

Person *person = [Person new];
[person setValue:@"fale" forKey:@"sex"];
NSLog(@"-->%@",[person valueForKey:@"sex"]);
[person setValue:@"170.0" forKey:@"_height"];
NSLog(@"-->%@",[person valueForKey:@"height"]);   

我們可以看到,Person的成員變量是 _sex_height,設(shè)值和取值的時候是否帶"_"效果都是一樣的藕咏,這跟KVC設(shè)置的機制有關(guān)状知。

  • 設(shè)值:優(yōu)先尋找 setter 方法,如果沒有該方法則尋找成員變量 _a侈离,如果仍然不存在试幽,則尋找成員變量 a,如果還是沒找到則會調(diào)用這個類的 setValue:forUndefinedKey: 方法卦碾,并且不管這些方法铺坞、成員變量是私有還是公有的甚至是只讀的都可以正確設(shè)置

優(yōu)先級為:setter方法--> _a --> a --> setValue:forUndefinedKey: 方法

  • 取值:優(yōu)先尋找 getter 方法,如果沒有找到該方法則尋找成員變量 _a洲胖,如果仍然不存在济榨,則尋找成員變量 a,如果還是沒有找到則會待用這個類 valueforUndefinedKey: 方法

優(yōu)先級為:getter方法--> _a --> a --> valueforUndefinedKey: 方法

復(fù)合路徑

如果Person中有一個Accont類绿映,表示賬戶余額擒滑,要怎么使用KVC呢?

@interface Account : NSObject
{
    float _balance; // 賬戶余額
}
@end

@interface Person : NSObject
@property (strong ,nonatomic) Account *account; // 賬戶余額
@end

主程序中

Person *person = [Person new];
[person setValue:@"1234.6" forKeyPath:@"account.balance"];
NSLog(@"-->%@",[person valueForKeyPath:@"account.balance"]);

鍵值監(jiān)聽KVO(NSKeyValueObserving)

作用:實現(xiàn)UI和數(shù)據(jù)模型的分離

KVO是一種觀察者模式叉弦,可以監(jiān)聽某對象的屬性值的變化丐一,當該屬性值發(fā)生變化時,作為監(jiān)聽者就可以做出相應(yīng)的響應(yīng)動作淹冰,利用這一模式库车,我們可以在MVC模式下實現(xiàn)Model和View的之間的通信,即當Model發(fā)生變化時樱拴,UI作為觀察者就可以發(fā)生相應(yīng)變化柠衍。

使用步驟:

  1. 注冊成觀察者
  2. 重寫監(jiān)聽回調(diào)方法
  3. 注銷觀察者

示例:

這里使用控制器作為觀察者,觀察某個模型的屬性來演示KVO的使用晶乔。

首先我們創(chuàng)建一個項目珍坊,并新建一個數(shù)據(jù)模型

@interface DataModel : NSObject
{
    NSString *_title;    
}
@end

步驟一:將控制器注冊為該模型的觀察者

self.model = [DataModel new];
[self.model addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil]; // 注冊為觀察者

步驟二:重寫KVO的監(jiān)聽回調(diào)

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"title"]&&object==self.model) {
        self.label.text = [change objectForKey:@"new"];
    }
    else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]
    }
}

步驟三:手動注銷觀察者

-(void)dealloc{
    [self.model removeObserver:self forKeyPath:@"title"];
}

這樣,當數(shù)據(jù)模型發(fā)生變化時正罢,我們就可以監(jiān)聽到阵漏,并作UI上的改變了

// 改變數(shù)據(jù)模型title的值
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [self.module setValue:@"a new value" forKey:@"title"];
});

運行結(jié)果

KVO示例

??注:不是所有的賦值方式都支持 KVO 監(jiān)聽。如果你直接通過成員變量進行賦值操作是無法監(jiān)聽到的翻具。

// 通過變量直接賦值 (不可以)
_title = @"hahahaha";
// 通過調(diào)用 setter 方法賦值(可以)
self.title = @"aaaa";
// 通過 KVC 賦值 (可以)
[self setValue:@"xxxxx" forKey:@"title"];

上述三種賦值方式袱饭,只有后兩種才會被監(jiān)聽到,這和 KVO 實現(xiàn)的底層原理有關(guān)呛占。

KVO 的優(yōu)化

系統(tǒng)提供的 KVO 使用方式有一些不便之處。一個是所有的變化回調(diào)都會經(jīng)過方法 -observeValueForKeyPath:ofObject:change:change context:懦趋,這也就意味著需要集中處理所有的屬性監(jiān)聽變化晾虑,我們需要通過 keyPathobject 甚至是 context 來落地何種處理方式,另一就是需要手動移除帜篇,這個對新手不是非常的友好糙捺,因為如果處理不當,經(jīng)常會發(fā)生意想不到的問題笙隙。

那么針對上述兩個不便之處進行一定的優(yōu)化洪灯。我的思路是如下。

  • 轉(zhuǎn)移監(jiān)聽者

將監(jiān)聽權(quán)利移交給其他對象竟痰,包括移除的部分签钩。

  • 事件回調(diào)

監(jiān)聽對象不應(yīng)處理具體業(yè)務(wù),通過回調(diào)將事件交給外部處理坏快。使用回調(diào)的另一個好處就是形成了一對一的情況铅檩,即:一個監(jiān)聽對象一個事件回調(diào),這樣避免了集中處理的窘境莽鸿。

根據(jù)上述思路昧旨,我的優(yōu)化代碼如下。

@class LLKVOHandle;
@interface LLKVO : NSObject
/// 返回值值需要被持有
+(LLKVOHandle*)addObserverTo:(NSObject*)obj forKey:(NSString*)keyPath block:(void(^)(NSDictionary*changes))block;
@end

///  KVO 的幾個必要數(shù)據(jù)
@interface LLKVOHandle : NSObject
/// 被觀察者
@property (nonatomic, strong) NSObject* target;
/// 觀察屬性
@property (nonatomic, copy) NSString* keyPath;
/// 屬性變化的回調(diào)
@property (nonatomic, copy) void (^block)(NSDictionary*);
/// 構(gòu)造器
-(instancetype)initWithTarget:(NSObject*)target keyPath:(NSString*)keyPath;
@end
@implementation LLKVO
+(LLKVOHandle*)addObserverTo:(NSObject*)obj forKey:(NSString*)keyPath block:(void (^)(NSDictionary *))block{
    LLKVOHandle* handle = [[LLKVOHandle alloc] initWithTarget:obj keyPath:keyPath];
    handle.block = block;
    return handle;
}
@end

@implementation LLKVOHandle
/// 構(gòu)造器
-(instancetype)initWithTarget:(NSObject*)target keyPath:(NSString*)keyPath{
    if (self = [super init]) {
        self.target = target;
        self.keyPath = keyPath;
        // 在當前類中進行監(jiān)聽
        [target addObserver:self forKeyPath:keyPath options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil];
    }
    return self;
}

// 處理 KVO 的事件
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (self.block) {
        self.block(change); // 將事件拋出
    }
}

/// 管理觀察者和被觀察者祥得,這里進行了移除清理操作
-(void)removeObserver{
    [self.target removeObserver:self forKeyPath:self.keyPath context:nil];
    self.target = nil;
}

/// 釋放之后自動移除觀察者
-(void)dealloc{
    [self removeObserver];
}
@end

其中兔沃,LLKVOHandle 就是新的監(jiān)聽對象,它需要做的就是添加監(jiān)聽事件级及,完成事件回調(diào)乒疏,移除清理。LLKVO 則是便捷入口创千。

那么缰雇,優(yōu)化過后的代碼,在使用時就會向下面這樣追驴。

self.kvoHandle = [LLKVO addObserverTo:self.model forKey:@"title" block:^(NSDictionary *changes) {
    // 處理屬性變化
    NSLog(@"%@", changes);
}];

需要注意一點的就是 block 的內(nèi)存泄漏問題械哟,需要弱引用。

KVO 的實現(xiàn)原理

KVO 的實現(xiàn)依賴于OC強大的運行時 Runtime殿雪。

原理:

當觀察某對象時暇咆,KVO動態(tài)創(chuàng)建該對象的子類,將原始類和子類的屬性設(shè)置setter 方法進行交互丙曙,并重寫子類被觀察屬性 setter 方法爸业,隨后通知觀察者該屬性的變化狀況。

因此運行時主要做了三件事亏镰。

  1. 創(chuàng)建子類
  2. 方法交換
  3. 通知觀察者

運行時在 KVO 機制中扮演了黑客的身份扯旷,截獲了用戶的信息,稍加改造之后繼續(xù)發(fā)出去索抓,但同時將用戶的信息解析之后販賣給了別人(監(jiān)聽者)钧忽。

實現(xiàn)過程:

Apple使用方法交換(isa-swizzling)來實現(xiàn)KVO毯炮。

當觀察對象A時,KVO動態(tài)創(chuàng)建了新的名為 NSKVONotifying_A 的新類耸黑,該類是對象A的子類桃煎,并且使用 swizzling 交換了所觀察的屬性的 setter 方法,KVO重寫了新類的觀察屬性的 setter 方法大刊,在調(diào)用原類中的 setter 方法后为迈,通知所有觀察者該屬性的變化情況。

  • NSKVONotifying_A

每個對象內(nèi)部都有 isa 指針缺菌,這個指針指向該對象的類葫辐,在KVO機制中,該isa指針被修改為指向系統(tǒng)新創(chuàng)建的子類 NSKVONotifying_A 男翰,那么當被觀察者修改被檢測的屬性的值時候另患,就會調(diào)用KVO重寫的setter方法,從而激活鍵值通知機制蛾绎,實現(xiàn)當前類屬性改變的監(jiān)聽昆箕。

所以當我們從應(yīng)用層面上來看,并沒有意識到有新類的出現(xiàn)租冠,這是apple隱瞞類對KVO的底層實現(xiàn)過程鹏倘,而我們還以為是原來的類,但是此時如果我們創(chuàng)建一個新的名為 NSKVONotifying_A 的類時顽爹,就會發(fā)現(xiàn)系統(tǒng)運行到注冊KVO的那段代碼時纤泵,程序發(fā)生崩潰,因為系統(tǒng)在注冊監(jiān)聽的時候動態(tài)創(chuàng)建了名為NSKVONotifying_A的中間類镜粤,并指向這個中間類了捏题。

  • 子類重寫 setter 方法

KVO的鍵值觀察通知依賴于NSObject的兩個方法:-willChangeValueForKey:didChangeValueForKey: ,在存取數(shù)值的前后分別調(diào)用2個方法肉渴;
被觀察屬性發(fā)生改變之前公荧,-willChangeValueForKey: 被調(diào)用,通知系統(tǒng)該keyPath的屬性值即將變更同规;當改變發(fā)生后循狰,-didChangeValueForKey: 被調(diào)用,通知系統(tǒng)keyPath的屬性值已經(jīng)發(fā)生改變券勺,之后绪钥,observeValueForKey:ofObject:change:context: 也會被調(diào)用。
注意:重寫觀察屬性的setter方法這種繼承方式的注入是在運行時而不是編譯時實現(xiàn)的

KVO 機制中需要借助重寫 setter 方法关炼,因此這也解釋了為什么直接使用變量進行賦值無法觸發(fā) KVO 通知程腹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市儒拂,隨后出現(xiàn)的幾起案子跪楞,更是在濱河造成了極大的恐慌缀去,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甸祭,死亡現(xiàn)場離奇詭異,居然都是意外死亡褥影,警方通過查閱死者的電腦和手機池户,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凡怎,“玉大人校焦,你說我怎么就攤上這事⊥车梗” “怎么了寨典?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長房匆。 經(jīng)常有香客問我耸成,道長,這世上最難降的妖魔是什么浴鸿? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任井氢,我火速辦了婚禮,結(jié)果婚禮上岳链,老公的妹妹穿的比我還像新娘花竞。我一直安慰自己,他們只是感情好掸哑,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布约急。 她就那樣靜靜地躺著,像睡著了一般苗分。 火紅的嫁衣襯著肌膚如雪厌蔽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天俭嘁,我揣著相機與錄音躺枕,去河邊找鬼。 笑死供填,一個胖子當著我的面吹牛拐云,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播近她,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼叉瘩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了粘捎?” 一聲冷哼從身側(cè)響起薇缅,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤危彩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后泳桦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汤徽,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年灸撰,在試婚紗的時候發(fā)現(xiàn)自己被綠了谒府。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡浮毯,死狀恐怖完疫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情债蓝,我是刑警寧澤壳鹤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站饰迹,受9級特大地震影響芳誓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹦锋,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一兆沙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧莉掂,春花似錦葛圃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至厘唾,卻和暖如春褥符,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抚垃。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工喷楣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鹤树。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓铣焊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親罕伯。 傳聞我的和親對象是個殘疾皇子曲伊,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

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

  • KCV 其實由于ObjC的語言特性,你根部不必進行任何操作就可以進行屬性的動態(tài)讀寫追他,這種方式就是Key Value...
    TYM閱讀 1,059評論 0 4
  • 本文結(jié)構(gòu)如下: Why? (為什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等開會閱讀 1,653評論 1 21
  • 目錄:1.KVC用法坟募;2.KVC和對象的setter岛蚤、getter方法的區(qū)別;3.key和keyPath的區(qū)別懈糯;4...
    倫倫子_f7b3閱讀 577評論 0 1
  • KVC簡介 我們知道可以通過setter涤妒、getter方法來設(shè)置和修改對象的屬性,也知道如何通過簡化的點語法來設(shè)置...
    請叫我周小帥閱讀 488評論 0 1
  • 在iOS開發(fā)中,我們常常用到鍵值編碼KVC和鍵值監(jiān)聽KVO兩個東東蜂奸,今天小編和大家分享的就是這兩個東東在應(yīng)用開發(fā)中...
    突然自我閱讀 1,002評論 2 3