KVC/KVO
概念
KVC : 即 Key-Value-Coding,用于鍵值編碼敬肚。作為 cocoa 的一個(gè)標(biāo)準(zhǔn)化組成部分毕荐,它是基于 NSKeyValueCoding 非正式協(xié)議的機(jī)制。簡單來說艳馒,就是直接通過 key 值對對象的屬性進(jìn)行存取操作憎亚,而不需要調(diào)用明確的存取方法(set 和 get 方法 )∨浚基本上所有的 OC 對象都支持 KVC第美。
KVO : 即 Key-Value-Observing ,鍵值觀察陆爽∈餐回調(diào)機(jī)制,當(dāng)指定的對象屬性(內(nèi)存地址/常量改變)被修改后慌闭,對象就會(huì)受到通知别威。使用 KVO 機(jī)制的前提:必須能支持 KVC 機(jī)制躯舔;另外,在 MVC 設(shè)計(jì)架構(gòu)中省古, KVO 適合 在 Model 和 Controller 之間通訊粥庄。
KVC / KVO 的使用
其實(shí),我們經(jīng)常會(huì)用到 KVC 和 KVO 機(jī)制來解決實(shí)際開發(fā)中的好多問題豺妓,尤其是 KVC惜互,因?yàn)檫@種基于運(yùn)行時(shí)的編程方式大大地提高了靈活性,簡化代碼琳拭;比如训堆,我們經(jīng)常會(huì)用如下代碼來修改 textField 的 placeHolder 的字體和顏色等屬性。
[self setValue:[UIColor whiteColor] forKeyPath:@"_placeholderLabel.textColor"];
[self setValue:[UIFont boldSystemFontOfSize:20] forKeyPath:@"_placeholderLabel.font"];
下面臀栈,來為大家列舉一下 KVC / KVO 的使用場景
KVC的使用
KVC 的常用的方法:
- (nullable id)valueForKey:(NSString *)key; //直接通過Key來取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通過Key來設(shè)值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通過KeyPath來設(shè)值
NSKeyValueCoding 類別中的一些其他方法:
+ (BOOL)accessInstanceVariablesDirectly;
//默認(rèn)返回YES,表示如果沒有找到Set<Key>方法的話挠乳,會(huì)按照_key权薯,_iskey,key睡扬,iskey的順序搜索成員盟蚣,設(shè)置成NO就不這樣搜索
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值正確性驗(yàn)證的API,它可以用來檢查set的值是否正確卖怜、為不正確的值做一個(gè)替換值或者拒絕設(shè)置新值并返回錯(cuò)誤原因屎开。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操作的API,里面還有一系列這樣的API马靠,如果屬性是一個(gè)NSMutableArray奄抽,那么可以用這個(gè)方法來返回。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在甩鳄,且沒有KVC無法搜索到任何和Key有關(guān)的字段或者屬性逞度,則會(huì)調(diào)用這個(gè)方法,默認(rèn)是拋出異常妙啃。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個(gè)方法一樣档泽,但這個(gè)方法是設(shè)值。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法時(shí)面給Value傳nil揖赴,則會(huì)調(diào)用這個(gè)方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,返回該組key對應(yīng)的Value馆匿,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典燥滑。
動(dòng)態(tài)地取值和設(shè)值
用 KVC 來訪問和修改私有變量
對于類里的私有屬性渐北,Objective-C是無法直接訪問的,但是KVC是可以的Model 和 字典轉(zhuǎn)換
其實(shí)铭拧,常見的第三方 模型解析庫就是利用 KVC 和 Objective-C 的 動(dòng)態(tài)性 Runtime 實(shí)現(xiàn)的腔稀。修改一些控件的內(nèi)部屬性
利用 KVC 來修改我們無法訪問和修改控件的樣式盆昙,但是 Apple 并沒有為我們提供訪問這些控件的 API,因此我們不好獲取這些屬性名焊虏。所以淡喜,我們又要使用 Runtime 這個(gè)東西了。比如诵闭,我們可以使用以下代碼來獲取 UITableView 的屬性列表炼团。
unsigned int count = 0;
objc_property_t *propertyList = class_copyPropertyList([UITableView class], &count);
NSMutableArray * mutableList_property = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i < count; i++) {
const char *propertyName = property_getName(propertyList[I]);
[mutableList_property addObject:[NSString stringWithUTF8String:propertyName]];
}
free(propertyList);
NSArray * propertylist = [NSArray arrayWithArray:mutableList_property];
NSLog(@"\n獲取UITableView的屬性列表:%@",propertylist);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
- 操作集合
蘋果 對于 KVC 的 valueForKey: 方法做了特殊的實(shí)現(xiàn)。常見的容器類就實(shí)現(xiàn)了這些方法疏尿。 - 實(shí)現(xiàn)高階消息傳遞
當(dāng)對容器使用 KVC 時(shí)瘟芝, valueForKey 會(huì)直接唄傳遞給 容器中的每一個(gè)對象,而不只是對容器本身進(jìn)行操作褥琐。結(jié)果會(huì)被添加進(jìn)返回的容器锌俱。
NSArray* arrStr = @[@"english",@"franch",@"chinese"];
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString* str in arrCapStr) {
NSLog(@"%@",str);
}
NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber* length in arrCapStrLength) {
NSLog(@"%ld",(long)length.integerValue);
}
打印結(jié)果
2016-04-20 16:29:14.239 KVCDemo[1356:118667] English
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Franch
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Chinese
2016-04-20 16:29:14.240 KVCDemo[1356:118667] 7
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 6
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 7
- 用 KVC 中的函數(shù)操作集合
KVC 提供了很復(fù)雜的函數(shù):- 簡單集合運(yùn)算符: @avg @count @max @min @sum (不支持自定義)
- 對象運(yùn)算級: @distinctUnionOfObjects @unionOfObjects (比集合運(yùn)算符稍微復(fù)雜,能以數(shù)組的方式返回指定的內(nèi)容)
- Array 和 Set 操作符: @distinctUnionOfArrays @unionOfArrays @distinctUnionOfSets
KVO 的使用
對于敌呈,KVO 的使用贸宏,就是觀察鍵值。在 MVC 設(shè)計(jì)架構(gòu)中磕洪, KVO 適合 在 Model 和 Controller 之間通訊吭练。
使用步驟:
- 注冊觀察者,實(shí)施監(jiān)聽
//第一個(gè)參數(shù)observer:觀察者 (這里觀察self.myKVO對象的屬性變化)
//第二個(gè)參數(shù)keyPath: 被觀察的屬性名稱(這里觀察self.myKVO中num屬性值的改變)
//第三個(gè)參數(shù)options: 觀察屬性的新值析显、舊值等的一些配置(枚舉值鲫咽,可以根據(jù)需要設(shè)置,例如這里可以使用兩項(xiàng))
//第四個(gè)參數(shù)context: 上下文谷异,可以為kvo的回調(diào)方法傳值(例如設(shè)定為一個(gè)放置數(shù)據(jù)的字典)
//注冊觀察者
[self.myKVO addObserver:self forKeyPath:@"num" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
- 在回調(diào)方法中處理屬性發(fā)生的變化
/keyPath:屬性名稱
//object:被觀察的對象
//change:變化前后的值都存儲(chǔ)在change字典中
//context:注冊觀察者時(shí)分尸,context傳過來的值
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
}
- 移除觀察者
注意:
觀察者觀察的是屬性,只有遵循 KVO 變更屬性值的方式才會(huì)執(zhí)行KVO的回調(diào)方法歹嘹,例如是否執(zhí)行了setter方法寓落、或者是否使用了KVC賦值。如果賦值沒有通過setter方法或者KVC荞下,而是直接修改屬性對應(yīng)的成員變量伶选,例如:僅調(diào)用_name = @"newName",這時(shí)是不會(huì)觸發(fā)kvo機(jī)制尖昏,更加不會(huì)調(diào)用回調(diào)方法的仰税。
實(shí)現(xiàn)原理
KVO 是 基于 KVC 實(shí)現(xiàn)的。首先了解 KVC 的機(jī)制抽诉,才能更好的理解 KVO陨簇。
KVC 是如何尋找 Key 值的
- 當(dāng)調(diào)用
setValue: forKey:
時(shí)- 優(yōu)先調(diào)用
set <Key>:屬性值
方法 ,代碼實(shí)現(xiàn) setter 方法 - 如果無 set 方法迹淌, KVC 會(huì)檢查
+ (BOOL)accessInstanceVariablesDirectly
有沒有返回 YES(默認(rèn)為 YES河绽,如果重寫使其返回 NO 的話己单,就會(huì)執(zhí)行setValue:forUndefinedKey:
)。KVC 機(jī)制會(huì)按照_key耙饰,_iskey纹笼,key,iskey
的順序搜索成員苟跪,對其賦值廷痘。 - 如果都不存在,系統(tǒng)會(huì)執(zhí)行該對象的
setValue:forUndefinedKey:
方法,默認(rèn)拋出異常。
- 優(yōu)先調(diào)用
-
當(dāng)調(diào)用
valueForKey:
時(shí):
KVC對key的搜索方式不同于setValue:屬性值 forKey:@”name“七问,其搜索方式如下:- 首先按
get<Key>,<key>,is<Key>
的順序方法查找 getter 方法,找到的話會(huì)直接調(diào)用债沮。如果是BOOL或者Int等值類型, 會(huì)將其包裝成一個(gè)NSNumber對象。 - 如果上面的 getter 方法未找到,KVC則會(huì)查找
countOf<Key> , objectIn<Key>AtIndex
或<Key>AtIndexes
格式的方法枢冤。如果 countOf<Key> 方法和另外兩個(gè)方法中的一個(gè)被找到,那么就會(huì)返回一個(gè)可以響應(yīng) NSArray 所有方法的代理集合(它是 NSKeyValueArray 歼狼,是NSArray的子類)掏导,調(diào)用這個(gè)代理集合的方法享怀,或者說給這個(gè)代理集合發(fā)送屬于NSArray的方法羽峰,就會(huì)以countOf<Key> ,objectIn<Key>AtIndex或<Key>AtIndexes這幾個(gè)方法組合的形式調(diào)用。還有一個(gè)可選的get<Key>: range:
方法添瓷。所以你想重新定義 KVC 的一些功能梅屉,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標(biāo)準(zhǔn)命名方法鳞贷,包括方法簽名坯汤。 - 如果上面的方法沒有找到,那么會(huì)同時(shí)查找
countOf<Key>搀愧,enumeratorOf<Key>,memberOf<Key>
格式的方法惰聂。如果這三個(gè)方法都找到,那么就返回一個(gè)可以響應(yīng)NSSet所的方法的代理集合咱筛,和上面一樣搓幌,給這個(gè)代理集合發(fā)NSSet的消息,就會(huì)以countOf<Key>迅箩,enumeratorOf<Key>,memberOf<Key>組合的形式調(diào)用溉愁。 - 如果還未找到,則和先前的設(shè)值一樣饲趋,會(huì)按
_<key>,_is<Key>,<key>,is<Key>
的順序搜索成員變量名拐揭,這里不推薦這么做撤蟆,因?yàn)檫@樣直接訪問實(shí)例變量破壞了封裝性,使代碼更脆弱堂污。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly
返回NO的話家肯,那么會(huì)直接調(diào)用valueForUndefinedKey:
- 還沒有找到的話,調(diào)用
valueForUndefinedKey:
- 首先按
在 KVC 中 使用 keyPath
用小數(shù)點(diǎn)分割 key,敷鸦,然后再像普通key一樣按照先前介紹的順序搜索下去息楔。如果屬性不存在,同樣會(huì)調(diào)用valueForUndefinedKey:
KVC處理異常
重寫拋出異常的那兩個(gè)方法扒披,通常情況下值依,KVC不允許你要在調(diào)用setValue:屬性值 forKey:@”name“(或者keyPath)時(shí)對非對象傳遞一個(gè)nil的值。KVC 處理非對象和自定義對象
valueForKey:
總是返回一個(gè)id對象碟案,如果原本的變量類型是值類型或者結(jié)構(gòu)體愿险,返回值會(huì)封裝成NSNumber或者NSValue對象,但是setValue:forKey:
卻不行价说。你必須手動(dòng)將值類型轉(zhuǎn)換成NSNumber或者NSValue類型辆亏,才能傳遞過去。-
KVC與容器類
不可變的有序容器屬性(NSArray)和無序容器屬性(NSSet)一般可以使用valueForKey:
來獲取鳖目。如有一個(gè)叫 items 的 NSArray 的屬性扮叨,你可以使用valuleaForKey:@"items"
來獲取這個(gè)屬性。
當(dāng)對象的屬性是可變的容器時(shí)领迈,對于有序容器彻磁,可以使用如下方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
該方法的返回值是一個(gè)可變的有序數(shù)組,如果調(diào)用該方法狸捅,KVC 的搜索順序如下:
1. 搜索insertObject: in<Key>AtIndex:
,removeObjectFrom<Key>AtIndex:
或者insert<Key>AtIndexes
衷蜓,remove<Key>AtIndexes
格式的方法。如果至少找到一個(gè) insert 方法和一個(gè) remove 方法尘喝,那么同樣返回一個(gè)可以響應(yīng) NSMutableArray 所有方法代理集合(類名是 NSKeyValueFastMutableArray2 )磁浇,那么給這個(gè)代理集合發(fā)送NSMutableArray 的方法,就會(huì)以insertObject:in<Key>AtIndex:
,removeObjectFrom<Key>AtIndex:
或者insert<Key>AdIndexes
,remove<Key>AtIndexes
組合的形式調(diào)用朽褪。還有兩個(gè)可選實(shí)現(xiàn)的接口:replaceOnjectAtIndex:withObject:
,replace<Key>AtIndexes:with<Key>:
置吓。- 如果上步的方法沒有找到,則搜索
set<Key>:
格式的方法缔赠,如果找到衍锚,那么發(fā)送給代理集合的 NSMutableArray 最終都會(huì)調(diào)用set<Key>:
方法。 也就是說橡淑,mutableArrayValueForKey:
取出的代理集合修改后构拳,用set<Key>:
重新賦值回去去。這樣做效率會(huì)低很多。所以推薦實(shí)現(xiàn)上面的方法置森。 - 如果上一步的方法還還沒有找到斗埂,再檢查類方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回 YES (默認(rèn)行為),會(huì)按_<key>,<key>
,的順序搜索成員變量名凫海,如果找到呛凶,那么發(fā)送的 NSMutableArray 消息方法直接交給這個(gè)成員變量處理。 - 如果還未找到行贪,則調(diào)用
valueForUndefinedKey:
崭捍,默認(rèn)拋出異常 橄浓。
- 如果上步的方法沒有找到,則搜索
除了用于有序的可變?nèi)萜魍猓?code>mutableArrayValueForKey:一般還用于對 NSMutableArray 添加 Observer 上准给。如果對象屬性是個(gè) 可變?nèi)萜黝愋蜁r(shí)叼旋,你給他添加 KVO時(shí),你會(huì)發(fā)現(xiàn)當(dāng)你添加或移除元素時(shí)并不會(huì)接受到變化。因?yàn)?KVO 的本質(zhì)是系統(tǒng)監(jiān)測到某個(gè)屬性的內(nèi)存地址或者常量改變時(shí),會(huì)添加上- (void)willChangeValueForKey:(NSString *)key
和- (void)didChangeValueForKey:(NSString *)key
方法來發(fā)送通知,所以一種方法是手動(dòng)調(diào)用這兩個(gè)方法慧瘤,一種是使用上述的 mutableArrayValueForKey:
方法怔匣。
對于無序容器時(shí)拴疤,可以使用下面的方法:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
該方法返回一個(gè)可變的無序數(shù)組,如果調(diào)用該方法独泞,KVC 的搜索順序除了檢查 receiver 是 ManagedObject 以外呐矾,其搜索順序和 mutableArrayValueForKey
基本一致。
- KVC與字典
當(dāng)對NSDictionary對象使用KVC時(shí)懦砂,valueForKey:的表現(xiàn)行為和objectForKey:一樣蜒犯。所以使用valueForKeyPath:用來訪問多層嵌套的字典是比較方便的。
KVO 的原理
我們之前就說了 KVO 的實(shí)現(xiàn)依靠的是 Objective-C 強(qiáng)大的 Runtime荞膘。那么具體的罚随,我們是如何實(shí)現(xiàn)這一機(jī)制的呢?下面讓我們共同來探討一下羽资。
基本原理:
當(dāng)觀察對象 A 時(shí)淘菩, KVO 機(jī)制動(dòng)態(tài)的創(chuàng)建了一個(gè)對象 A 當(dāng)前的子類,并為這個(gè)新的子類重寫了被觀察屬性 keyPath 的 setter 方法屠升。在 setter 方法中潮改,我們通知觀察對象屬性的改變狀態(tài)。
進(jìn)一步剖析:
其實(shí) Apple 使用了 isa 混寫腹暖,即 isa-swizzling 來實(shí)現(xiàn) KVO汇在。
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.
那么,我們通過下面的代碼來看看脏答,isa-swizzling 的真面目到底是什么糕殉?
// 在 PQCPeople.m 中,我們模擬 KVO 的過程亩鬼,打印 isa 指針指向的類,以及 setter 方法的的函數(shù)指針
- (void)printUserInfo {
NSLog(@"isa:%@,supperclass:%@",NSStringFromClass(object_getClass(self)),
class_getSuperclass(object_getClass(self)));
NSLog(@"self:%@, [self superclass]:%@", self, [self superclass]);
NSLog(@"name setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setNamestr:)));
NSLog(@"printInfo function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(printUserInfo)));
}
在運(yùn)行過程中阿蝶,在添加Observer前辛孵,添加Observer以及刪除Observer后分別打印出該類的信息。
PQCPeople *person = [[PQCPeople alloc]init];
NSLog(@"Before add observer————————————————————————–");
[person printUserInfo];
[person addObserver:self forKeyPath:@"namestr" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"After add observer————————————————————————–");
[person printUserInfo];
[person removeObserver:self forKeyPath:@"namestr"];
NSLog(@"After remove observer————————————————————————–");
[person printUserInfo];
以下是打印的信息:
2017-11-14 13:06:51.927605+0800 KVC&KVO[851:92433] Before add observer————————————————————————–
2017-11-14 13:06:51.927701+0800 KVC&KVO[851:92433] isa:PQCPeople,supperclass:NSObject
2017-11-14 13:06:51.927814+0800 KVC&KVO[851:92433] self:<PQCPeople: 0x604000059c50>, [self superclass]:NSObject
2017-11-14 13:06:51.927904+0800 KVC&KVO[851:92433] name setter function pointer:0x10c2429d0
2017-11-14 13:06:51.927987+0800 KVC&KVO[851:92433] printInfo function pointer:0x10c242ba0
2017-11-14 13:06:51.928224+0800 KVC&KVO[851:92433] After add observer————————————————————————–
2017-11-14 13:06:51.928316+0800 KVC&KVO[851:92433] isa:NSKVONotifying_PQCPeople,supperclass:PQCPeople
2017-11-14 13:06:51.928489+0800 KVC&KVO[851:92433] self:<PQCPeople: 0x604000059c50>, [self superclass]:NSObject
2017-11-14 13:06:51.928628+0800 KVC&KVO[851:92433] name setter function pointer:0x10c58f666
2017-11-14 13:06:51.928798+0800 KVC&KVO[851:92433] printInfo function pointer:0x10c242ba0
2017-11-14 13:06:51.928954+0800 KVC&KVO[851:92433] After remove observer————————————————————————–
2017-11-14 13:06:51.929077+0800 KVC&KVO[851:92433] isa:PQCPeople,supperclass:NSObject
2017-11-14 13:06:51.929268+0800 KVC&KVO[851:92433] self:<PQCPeople: 0x604000059c50>, [self superclass]:NSObject
2017-11-14 13:06:51.929431+0800 KVC&KVO[851:92433] name setter function pointer:0x10c2429d0
2017-11-14 13:06:51.929586+0800 KVC&KVO[851:92433] printInfo function pointer:0x10c242ba0
通過分析赡磅,我們會(huì)發(fā)現(xiàn)在添加KVO之后魄缚,isa 已經(jīng)替換成了NSKVONotifying_PQCPeople
,而根據(jù) class_getSuperclass
得到的結(jié)果竟然是 PQCPerson, 然后 namestr 是我們KVO需要觀察的屬性,它的 setter
函數(shù)指針也變了焚廊。
我們上面也說道冶匹, OC 的消息機(jī)制是通過 isa 去查找實(shí)現(xiàn)的,那么我們可以根據(jù)以上的分析咆瘟,可以大致得出嚼隘,KVO的實(shí)現(xiàn)應(yīng)該是:
添加 Observe
通過 runtime 偷偷實(shí)現(xiàn)了一個(gè)子類,并且以 NSKVONotifying_+類名 來命名袒餐;
將之前那個(gè)對象的isa指針指向了這個(gè)子類飞蛹;
重寫了觀察的對象setter方法,并且在重寫的中添加了willChangeValueForKey:
以及didChangeValueForKey:
移除 Observe
將 isa 的指向指回原來的類對象中灸眼。
因此卧檐,我們的 KVO 就是通過如圖這種過程,進(jìn)行鍵值觀察焰宣。
文末
通過上面的分析霉囚,我們也大概了解了 KVC 以及 KVO 的實(shí)現(xiàn)過程以及實(shí)現(xiàn)原理,對這個(gè)感興趣的同學(xué)匕积,我們可以試著去自己實(shí)現(xiàn)一下 KVC 以及 KVO盈罐。