iOS:KVO與KVC

KVC

kvc全稱是 key value coding漾唉,又稱“鍵值編碼”,可以通過key獲取或修改其對應(yīng)值涩搓,因此會破壞面向?qū)ο笏枷搿?/p>

它提供一種機制可以間接訪問對象的屬性蛙紫,而不是通過setter/getter方法。

常見API

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

key和keyPah的區(qū)別

key:僅用于直接訪問對象的屬性荷辕,不能用于訪問嵌套的屬性或集合屬性的元素凿跳。

keyPath:除了可以訪問屬性,還可以訪問嵌套的屬性或集合屬性的元素疮方。

@interface HFPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) HFAnimal *animal;

@end

@interface HFAnimal : NSObject
@property (nonatomic, copy) NSString *type;
@end
HFPerson *person = [[HFPerson alloc] init];
[person setValue:@"John" forKey:@"name"]; // 使用 setValue:forKey: 設(shè)置 name 屬性
[person setValue:@30 forKey:@"age"]; // 使用 setValue:forKey: 設(shè)置 age 屬性

HFAnimal *animal = [[HFAnimal alloc] init];
animal.type = @"Cat";
person.animal = animal; // 設(shè)置 animal 屬性
        
NSString *name = [person valueForKey:@"name"]; // 使用 valueForKey: 獲取 name 屬性值
NSInteger age = [[person valueForKey:@"age"] integerValue]; // 使用 valueForKey: 獲取 age 屬性值
NSString *type = [person valueForKeyPath:@"animal.type"]; // 使用 valueForKeyPath: 獲取嵌套屬性值

NSString *name = [person valueForKey:@"name"]; // 使用 valueForKey: 獲取 name 屬性值

NSString *type = [person valueForKeyPath:@"animal.type"]; // 使用 valueForKeyPath: 獲取嵌套屬性值

可以直觀的發(fā)現(xiàn)key和keyPath的不同控嗜。

KVC原理

1、-(void)setValue:(id)value forKey:(NSString *)key;

首先按照setKey骡显,_setKey的順序查找setter方法疆栏,找到方法則直接調(diào)用并賦值

若未找到方法,則調(diào)用+(BOOL)accessInstanceVariablesDirectly方法判斷惫谤,是否可以直接訪問成員變量壁顶,默認(rèn)YES。

若accessInstanceVariablesDirectly返回YES石挂,則按照_key博助、_isKey、key痹愚、_isKey的順序查找成員變量富岳,找到直接賦值,未找到或accessInstanceVariablesDirectly返回NO拋出NSUnknownKeyException異常就會調(diào)用setValue:forUndefinedKey:并拋出NSUnknownKeyException異常拯腮。

2窖式、-(nullable id)valueForKey:(NSString *)key

首先會按照getKey、key动壤、isKey萝喘、_key的順序找到getter方法,若找到直接調(diào)用取值。

若未找到阁簸,則調(diào)用+(BOOL)accessInstanceVariablesDirectly方法判斷爬早,是否可以直接訪問成員變量,默認(rèn)YES启妹。

若返回YES筛严,則按照_key、_iskey饶米、key桨啃、iskey的順序匹配實例變量。如果找到這樣的實例變量檬输,則返回接收器中實例變量的值照瘾,若未找到或返回NO則調(diào)用-valueForUndefinedKey:,并拋出NSUnknownKeyException異常丧慈。


[圖片上傳中...(836c8db8b5bc1f415847a8628e1c79ce.png-39928c-1713449155581-0)]

注意事項

key的值必須正確析命,如果拼寫錯誤,就會出現(xiàn)異常伊滋。
當(dāng)key的值是未定義的碳却,會調(diào)用valueForUndefinedKey,如果重寫了這個方法笑旺,key的值出錯的時候會調(diào)用到此處。

因為類可以反復(fù)嵌套馍资,所以有keyPath筒主,用路徑實現(xiàn)訪問。

可以通過kvc訪問私有成員變量/屬性鸟蟹。

如果參數(shù)類型不是對象指針類型乌妙,但值為nil,則調(diào)用setNilValueForKey:建钥,-setNilValueForKey:的默認(rèn)實現(xiàn)引發(fā)NSInvalidArgumentException藤韵,我們可以重寫setNilValueForKey避免非對象傳遞nil出現(xiàn)的錯誤。對象傳遞nil不會調(diào)用此方法熊经,會直接報錯泽艘。

處理非對象,setValue時镐依,如果要賦值的是基本數(shù)據(jù)類型匹涮,需要封裝成NSNumber或NSValue類型,valueForKey時槐壳,返回的是id類型的對象然低,基本數(shù)據(jù)類型也會被封裝成NSNumber或NSValue類型。valueForKey可以自動將值封裝成對象,但是setValye不行雳攘,必須手動轉(zhuǎn)換再執(zhí)行setValue带兜。

KVC的更多應(yīng)用場景

1、批量操作

批量存值:dictionaryWithValuesForKeys

批量賦值:setValuesForKeysWithDictionary

HFPerson *person = [[HFPerson alloc] init];
[person setValue:@"John" forKey:@"name"]; // 使用 setValue:forKey: 設(shè)置 name 屬性
[person setValue:@30 forKey:@"age"]; // 使用 setValue:forKey: 設(shè)置 age 屬性
        
NSDictionary *dicOne = [person dictionaryWithValuesForKeys:@[@"name",@"age"]];
NSLog(@"dicOne = %@",dicOne);
        
NSDictionary *dicTwo = @{@"name":@"Mike",@"age":@35};
HFPerson *personTwo = [[HFPerson alloc] init];
[personTwo setValuesForKeysWithDictionary:dicTwo];
NSLog(@"personTwo.name = %@, age = %ld",personTwo.name,personTwo.age);

輸出結(jié)果

dicOne = {
    age = 30;
    name = John;
}
personTwo.name = Mike, age = 35

2吨灭、字典模型互轉(zhuǎn)

如果模型和dic不匹配

字典轉(zhuǎn)模型:重寫setValue:forUndefinedKey
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if([key isEqualToString:@"nickname"]) {
        self.name = (NSString *)value;
    }
}
NSDictionary *dicTwo = @{@"nickname":@"Mike",@"age":@35};
HFPerson *personTwo = [[HFPerson alloc] init];
[personTwo setValuesForKeysWithDictionary:dicTwo];
NSLog(@"personTwo.name = %@, age = %ld",personTwo.name,personTwo.age);

輸出personTwo.name = Mike, age = 35
模型轉(zhuǎn)字典刚照,重寫valueForUndefinedKey方法
- (nullable id)valueForUndefinedKey:(NSString *)key {
    if([key isEqualToString:@"nickname"]) {
        return self.name;
    }
    return nil;
}
HFPerson *person = [[HFPerson alloc] init];
[person setValue:@"John" forKey:@"name"];
[person setValue:@30 forKey:@"age"];
NSDictionary *tmp = [person dictionaryWithValuesForKeys:@[@"nickname",@"age"]];
NSLog(@"tmp = %@",tmp);

輸出結(jié)果:
tmp = {
    age = 30;
    nickname = John;
}

KVO

KVO全稱是Key-Value-Observing,又稱“鍵值監(jiān)聽”沃于,可以用于監(jiān)聽某個對象屬性值的改變涩咖。

KVO和NSNotificationCenter都是iOS觀察者模式的一種實現(xiàn),區(qū)別是NSNotificationCenter一對多繁莹,KVO是一對一檩互。

常見API

//1、注冊KVO監(jiān)聽
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
//2咨演、KVO監(jiān)聽實現(xiàn)
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
//3闸昨、移除KVO監(jiān)聽
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

1、注冊KVO監(jiān)聽

observer:觀察者薄风,監(jiān)聽屬性變化的對象饵较,該對象必須實現(xiàn)observeValueForKeyPath:ofObject:change:context方法。

keyPath:被觀察的屬性名稱遭赂,要和聲明的屬性名稱一致循诉。

options:回調(diào)方法中收到被觀察者屬性的舊值或新值等,用于指定觀察行為撇他。

context:上下文標(biāo)識符茄猫,用于區(qū)分不同的觀察者,可為空困肩。

2划纽、KVO監(jiān)聽實現(xiàn)

keyPath:被觀察的屬性名稱,要和聲明的屬性名稱一致锌畸。

object:被觀察的對象change:回調(diào)方法中收到被觀察者屬性的舊值或新值等勇劣,用于指定觀察行為。

context:上下文標(biāo)識符潭枣,用于區(qū)分不同的觀察者比默,可為空。

3卸耘、移除KVO監(jiān)聽

observer:要移除的觀察者
keyPath:要移除觀察的屬性名稱退敦,要和聲明的屬性名稱一致。
context:上下文標(biāo)識符蚣抗,用于區(qū)分不同的觀察者侈百,可為空

注意:

1瓮下、removeObserver需要在觀察者消失之前,否則會crash钝域。

2讽坏、add和remove成對出現(xiàn)。

KVO原理

KVO的本質(zhì)是為了改變setter方法的調(diào)用例证,實現(xiàn)原理就是當(dāng)調(diào)用addObserverForKeyPath時路呜,系統(tǒng)利用iisa混寫技術(shù)(isa-swizzling),在運行時動態(tài)創(chuàng)建NSKVONotifying_A,將原來A的isa指向NSKVONotifying_A织咧,NSKVONotifying_A的superClass指向原來的類胀葱。重寫NSKVONotifying_A的setter方法,通過重寫setter方法笙蒙,達(dá)到可以通知所有觀察者的目的抵屿。

@interface HFPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)printDescription;
@end

#import "HFPerson.h"
#import <objc/runtime.h>
@implementation HFPerson
- (void)printDescription {
    NSLog(@"isa = %@, super class = %@", NSStringFromClass(object_getClass(self)), NSStringFromClass(class_getSuperclass(object_getClass(self))));
    NSLog(@"self = %@, [self superclass] = %@",self, [self superclass]);
    NSLog(@"age setter pointer: %p", class_getMethodImplementation(object_getClass(self), @selector(setAge:)));
    NSLog(@"name setter pointer: %p", class_getMethodImplementation(object_getClass(self), @selector(setName:)));
    NSLog(@"printDescription pointer: %p", class_getMethodImplementation(object_getClass(self), @selector(printDescription)));
}
@end
@property (nonatomic, strong) HFPerson *person;
self.person = [[HFPerson alloc]init];
self.person.name = @"HaiFei";
    //
NSLog(@" - - - 監(jiān)聽前打印 - - - ");
[self.person printDescription];
    
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld context:nil];
self.person.name = @"haifei";
NSLog(@" - - - 監(jiān)聽后打印 - - - ");
[self.person printDescription];

[self.person removeObserver:self forKeyPath:@"name"];
NSLog(@" - - - 移除監(jiān)聽后打印 - - - ");
[self.person printDescription];

// 實現(xiàn)觀察者方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"name changed to: %@", change[NSKeyValueChangeNewKey]);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

輸出打印

- - - 監(jiān)聽前打印 - - - 
isa = HFPerson, super class = NSObject
self = <HFPerson: 0x6000037307a0>, [self superclass] = NSObject
age setter pointer: 0x10ea348b0
name setter pointer: 0x10ea34860
printDescription pointer: 0x10ea34710
name changed to: haifei
- - - 監(jiān)聽后打印 - - - 
isa = NSKVONotifying_HFPerson, super class = HFPerson
self = <HFPerson: 0x6000037307a0>, [self superclass] = NSObject
age setter pointer: 0x10ea348b0
name setter pointer: 0x7ff800bdd491
printDescription pointer: 0x10ea34710
- - - 移除監(jiān)聽后打印 - - - 
isa = HFPerson, super class = NSObject
self = <HFPerson: 0x6000037307a0>, [self superclass] = NSObject
age setter pointer: 0x10ea348b0
name setter pointer: 0x10ea34860
printDescription pointer: 0x10ea34710

代碼分析可知,創(chuàng)建監(jiān)聽后捅位,創(chuàng)建了新的類isa指向NSKVONotifying_HFPerson轧葛,并且監(jiān)聽屬性的setter方法也發(fā)生了改變,從而達(dá)到了監(jiān)聽的目的艇搀。
在移除監(jiān)聽后isa又指向原來的類尿扯,監(jiān)聽屬性的setter方法地址也變?yōu)樵瓉淼牡刂贰?br> 整個過程中未被監(jiān)聽的屬性的setter方法地址沒有變化,大概可以推斷出新的類通過superClass指針獲取原類的setter地址焰雕。

核心實現(xiàn)是NSKVONotifying_HFPerson內(nèi)重寫setter方法

- (void)setName:(NSString *)name {
    //表示屬性值即將發(fā)生變化 -keyPath和注冊kvo時key的一致
    [self willChangeValueForKey:@"keyPath"];
    //調(diào)用父類setter方法,即原類實現(xiàn)
    
    //表示屬性值已經(jīng)發(fā)生變化
    [self didChangeValueForKey:@"keyPath"];
}

常見問題

1衷笋、直接修改成員變量的值,會不會觸發(fā)KVO

不會矩屁。KVO本質(zhì)是生成找一個子類右莱,重寫父類的setter方法,直接修改員變量的值档插,不會觸發(fā)setter方法。

但是可以仿寫亚再,在賦值前后加上
willChangeValueForKey
賦值
didChangeValueForKey

2郭膛、KVC修改屬性會觸發(fā)KVO嗎?

會氛悬,在使用setValue:ForKey:修改屬性的時候则剃,會調(diào)用到屬性的setter方法,最終觸發(fā)到監(jiān)聽回調(diào)事件如捅。

3棍现、KVO和KVC的keypath一定是屬性嗎?

KVC支持實例變量镜遣,KVO只能手動支持實例變量的KVO監(jiān)聽己肮。

4、KVO與代理的效率對比

KVO的效率比代理低,因為KVO需要動態(tài)的生產(chǎn)中間類谎僻,比較耗時娄柳。

5、KVO是如何監(jiān)聽數(shù)組元素變化的艘绍?

需要搭配KVC的mutableArrayValueForKey獲得被監(jiān)聽屬性赤拒,然后更新。

self.mutArray = [NSMutableArray arrayWithObject:@"one"];
[self addObserver:self forKeyPath:@"mutArray" options:NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld context:nil];
[[self mutableArrayValueForKey:@"mutArray"]addObject:@"two"];
[self removeObserver:self forKeyPath:@"mutArray"];

// 實現(xiàn)觀察者方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"mutArray"]) {
        NSLog(@"tmpName changed to: %@", change[NSKeyValueChangeNewKey]);
        NSLog(@"self.mutArray = %@",self.mutArray);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

打印輸出
tmpName changed to: (
    two
)
self.mutArray = (
    one,
    two
)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末诱鞠,一起剝皮案震驚了整個濱河市挎挖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌航夺,老刑警劉巖蕉朵,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異敷存,居然都是意外死亡墓造,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門锚烦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來觅闽,“玉大人,你說我怎么就攤上這事涮俄◎茸荆” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵彻亲,是天一觀的道長孕锄。 經(jīng)常有香客問我,道長苞尝,這世上最難降的妖魔是什么畸肆? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮宙址,結(jié)果婚禮上轴脐,老公的妹妹穿的比我還像新娘。我一直安慰自己抡砂,他們只是感情好大咱,可當(dāng)我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著注益,像睡著了一般碴巾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上丑搔,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天厦瓢,我揣著相機與錄音提揍,去河邊找鬼。 笑死旷痕,一個胖子當(dāng)著我的面吹牛碳锈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播欺抗,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼售碳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绞呈?” 一聲冷哼從身側(cè)響起贸人,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎佃声,沒想到半個月后艺智,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡圾亏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年十拣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片志鹃。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡夭问,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出曹铃,到底是詐尸還是另有隱情缰趋,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布陕见,位于F島的核電站秘血,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏评甜。R本人自食惡果不足惜灰粮,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望忍坷。 院中可真熱鬧谋竖,春花似錦、人聲如沸承匣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽韧骗。三九已至,卻和暖如春零聚,著一層夾襖步出監(jiān)牢的瞬間袍暴,已是汗流浹背些侍。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留政模,地道東北人岗宣。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像淋样,于是被迫代替她去往敵國和親耗式。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,658評論 2 350

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

  • KVC KVC定義 KVC(Key-value coding)鍵值編碼趁猴,就是指iOS的開發(fā)中刊咳,可以允許開發(fā)者通過K...
    戀空K閱讀 721評論 0 2
  • 1. KVO 一.KVO原理的使用與證明 我們在開發(fā)的過程中經(jīng)常使用KVO和KVC,但是我們并不了解其底層原理和功...
    周灬閱讀 841評論 0 9
  • 【原創(chuàng)博文娱挨,轉(zhuǎn)載請注明出處!】之前做iOS開發(fā)的時候經(jīng)常使用KVO來監(jiān)聽對象屬性值的變化去執(zhí)行一些操作捕犬,但是從未思...
    RephontilZhou閱讀 1,095評論 1 9
  • 一碉碉、KVO KVO 的作用: kvo 就是監(jiān)聽某個對象的屬性柴钻,在該屬性的值發(fā)生變化時,通知觀察者誉裆。 KVO 的簡單...
    MonStar丶閱讀 1,658評論 0 7
  • KVO 基本使用 給 person 對象添加KVO監(jiān)聽 當(dāng)監(jiān)聽對象的屬性值發(fā)生改變時顿颅,就會調(diào)用。 移除監(jiān)聽 本質(zhì)分...
    gaookey閱讀 705評論 0 1