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
)