在開發(fā)過程中,KVC支持我們使用字符串作為關(guān)聯(lián)標(biāo)識為對象的某個實例變量或?qū)傩赃M(jìn)行賦值,這個字符串可以是對象的某個屬性名或?qū)嵗兞棵?,本文我們將通過官方文檔描述來探尋KVC賦值邏輯。
設(shè)置器方法:- (void)setValue:(nullable id)value forKey:(NSString *)key;
此方法根據(jù)關(guān)聯(lián)標(biāo)識字符串 key 以及設(shè)置值 value, 方法內(nèi)部通過以下三步進(jìn)行賦值操作:
1.查找設(shè)置器方法。
根據(jù)方法參數(shù)key的值去依次匹配以下方法:
// 注意:以下<Key>為 - (void)setValue:(nullable id)value forKey:(NSString *)key; 中提供的key值
// 僅僅只是將首字母大寫(如果以字母開頭)并替換,并不會對key值做其他額外操作來匹配存取器方法
// 例如key值以“_”下劃線開頭,例如“_name”,則匹配的方法為 -(void)set_name:(id)value; 2、3同理
// 1.
- (void)set<Key>:(id)value;
// 2.
- (void)_set<Key>:(id)value;
// 3.
- (void)setIs<Key>:(id)value;
如果找到對應(yīng)方法,則將value作為參數(shù)調(diào)用此方法。此步驟不關(guān)心類中是否擁有相應(yīng)的屬性或成員變量,僅僅只是方法匹配。
2.查找相應(yīng)的實例變量
第一步中沒有找到相關(guān)設(shè)置器方法并且該類 accessInstanceVariablesDirectly 屬性返回YES,那么將按照 _<key>、 _is<Key>、 <key>、 is<Key> 的順序查找相匹配的實例變量,如果找到相應(yīng)的實例變量則對變量進(jìn)行賦值(注意:此過程直接對實例變量進(jìn)行賦值,不調(diào)用setter)。
3.-setValue:forUndefinedKey:
如果經(jīng)過步驟1和步驟2沒有找到相應(yīng)的屬性設(shè)置器或者實例變量,-setValue:forUndefinedKey: 會被調(diào)用。
-setValue:forUndefinedKey: 方法默認(rèn)實現(xiàn)為拋出一個 NSUndefinedKeyException 異常??梢酝ㄟ^重寫此方法進(jìn)行特殊處理或者空實現(xiàn)避免拋出異常。
- (void)setValue:(nullable id)value forKey:(NSString *)key 方法賦值過程中針對非OC對象的處理
- 設(shè)置器方法value參數(shù)是一個OC對象,但是有時我們需要設(shè)置的實例變量類型有可能是基本數(shù)據(jù)類型、結(jié)構(gòu)體等,對于這種情況我們需要將值包裝成為NSNumber或者NSValue對象,設(shè)置器方法內(nèi)部會在調(diào)用存取器方法或為實例變量賦值之前對value進(jìn)行逆轉(zhuǎn)換操作。
- 當(dāng)檢查發(fā)現(xiàn)存取器方法參數(shù)或?qū)嵗兞款愋蜑榉菍ο箢愋停⑶襳alue為nil則 -setNilValueForKey: 方法會被調(diào)用。-setNilValueForKey: 方法默認(rèn)實現(xiàn)為拋出一個 NSInvalidArgumentException 異常,我們可以通過重寫此方法將nil映射為有意義的值。
如果一個集合類型對象調(diào)用此方法,則集合中每一個對象都會將 value、 key 作為參數(shù)調(diào)用設(shè)置器方法。
NSArray * arr = [NSArray arrayWithObjects: obj1, obj2, nil];
[arr setValue:@"zh-cn" forKey:@"language"];
集合中每一個對象的 setValue:forKey: 方法都會被調(diào)用,等同于 :
NSArray * arr = [NSArray arrayWithObjects: obj1, obj2, nil];
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[obj setValue:@"zh-cn" forKey:@"language"];
}];
訪問器方法:- (nullable id)valueForKey:(NSString *)key;
此方法根據(jù)關(guān)聯(lián)標(biāo)識字符串 key 取值,方法內(nèi)部通過以下四個步驟進(jìn)行取值(僅討論iOS):
1.查找訪問器方法
根據(jù)方法參數(shù)key的值依次匹配以下方法:
-get<Key>
-<key>
-is<Key>
-_<key>
如果找到對應(yīng)的訪問器方法,則調(diào)用此方法獲取返回值。此步驟依舊不關(guān)心類中是否擁有相應(yīng)的屬性或成員變量,僅僅匹配方法。
2.查找集合訪問方法
如果第一步中沒有找到相關(guān)訪問器方法,則匹配查找集合訪問器方法
-countOf<Key>
-objectIn<Key>AtIndex:
如果找到以上兩個方法則返回一個 NSKeyValueArray 類型的集合代理對象。NSKeyValueArray類繼承自NSArray,可以響應(yīng)NSArray所有消息,發(fā)送至集合代理對象的所有NSArray消息會被轉(zhuǎn)換為以上一個方法或兩個方法的組合發(fā)送至 -valueForKey: 方法的原始接收方。
關(guān)于 NSKeyValueArray 的更多內(nèi)容將在后面的部分講到,這里我們只需要簡單理解為 NSKeyValueArray 可以被當(dāng)做 NSArray 使用。
3.查找相應(yīng)的實例變量 (與設(shè)置器方法類似)
如果沒有找到相關(guān)訪問器方法并且該類 accessInstanceVariablesDirectly 屬性返回YES,那么將按照 _<key>、 _is<Key>、 <key>、 is<Key> 的順序查找相匹配的實例變量,如果找到相應(yīng)的實例變量則對變量進(jìn)行賦值(注意:此過程直接為實例變量賦值,不調(diào)用getter)。
4.- (id)valueForUndefinedKey:(NSString *)key
當(dāng)經(jīng)過以上步驟沒有找到訪問方法或?qū)嵗兞繒r,- (id)valueForUndefinedKey:(NSString *)key 會被調(diào)用,此方法默認(rèn)實現(xiàn)為拋出 NSUndefinedKeyException 異常。
- (void)setValue:(nullable id)value forKey:(NSString *)key 方法取值過程中針對非OC對象的處理
- 與設(shè)置方法同理,當(dāng)訪問方法取得的值為非OC對象類型時,如果結(jié)果的類型是NSNumber支持的數(shù)據(jù)類型之一,則將結(jié)果轉(zhuǎn)換為NSNumber對象返回,其他情況則將結(jié)果轉(zhuǎn)換為NSValue對象返回。
當(dāng)訪問方法的接收對象為集合時,方法返回值為集合中每一個元素通過訪問方法獲取的值的集合
NSArray * arr = [NSArray arrayWithObjects: obj1, obj2, nil];
NSArray * value = [arr valueForKey:@"language"];
集合中每一個對象的 valueForKey: 方法都會被調(diào)用,等同于 :
NSArray * arr = [NSArray arrayWithObjects: obj1, obj2, nil];
NSMutableArray * arrM = [NSMutableArray array];
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[arrM addObject: [obj valueForKey:@"language"]];
}];
NSArray * value = arrM.copy;
NSKeyValueArray 集合代理對象

1. NSKeyValueArray 繼承自 NSArray ,NSKeyValueArray 集合代理對象中保存了 -valueForKey: 方法原始消息接收方(_container)以及方法參數(shù)( _key )。
當(dāng)集合代理對象接收到 NSArray 消息時,消息會被轉(zhuǎn)換為 -countOf<Key> 和
-objectIn<Key>AtIndex: 的一個或多個消息組合發(fā)送至 -valueForKey: 方法原始接收方(_container 實例變量)。
例如:
__kindof NSArray * value = [self valueForKey:@"list"];
id element = [value objectAtIndex:0];
此時 -objectAtIndex: 會被轉(zhuǎn)換為 - objectInListAtIndex: 并發(fā)送至 - valueForKey: 方法的原始接收方self ,相當(dāng)于:
id element = [self objectInListAtIndex:0];
2. 下標(biāo)越界問題
通常我們在獲取數(shù)組中某個下標(biāo)元素時提供的下標(biāo)值超出了數(shù)組長度會拋出異常導(dǎo)致程序崩潰。

對于 NSKeyValueArray " [ ] " 和 " - objectAtIndex: "消息會被轉(zhuǎn)換為 - objectIn<Key>AtIndex: 發(fā)送至原始接收方( _container ),無論下標(biāo)是否超出了 - countOfList: 方法返回的長度都不會導(dǎo)致程序崩潰。因此在 - objectIn<Key>AtIndex: 方法中我們應(yīng)該先對 index 值進(jìn)行越界檢查,避免由于下標(biāo)越界而出現(xiàn)一些匪夷所思的BUG。
3. NSKeyValueArray獲取集合元素
在上面的文章中我們提到 NSKeyValueArray 繼承自 NSArray 可以響應(yīng)所有的 NSArray 消息(消息會被轉(zhuǎn)換為集合訪問器方法的一個或多個組合發(fā)送至 -valueForKey: 的原始接收方),因此每一次獲取 NSKeyValueArray 集合中的全部或某個元素時,原始接收方( _container )的 -objectIn<Key>AtIndex: 方法會都被調(diào)用,換言之 NSKeyValueArray 對象并不像 NSArray對象一樣會對集合中元素進(jìn)行強引用, NSKeyValueArray 僅僅只是一個代理對象,所有元素均通過 -objectIn<Key>AtIndex: 方法實時獲取,最終取得值由原始接收方?jīng)Q定。
此處我們將通過一個簡單的例子驗證以上結(jié)論,仔細(xì)思考一下,對于下面的例子element1和element2是同一個對象嗎?
- (void)test {
// 注意,此時我們的類中并沒有名為_list的成員變量,也沒有訪問器方法。
__kindof NSArray * value = [self valueForKey:@"list"];
// element1 和 element2 是同一個對象嗎?
id element1 = value[0];
id element2 = value[0];
NSLog(@"\nelement1:%@\nelement2:%@", element1, element2);
}
- (NSUInteger)countOfList {
return 1;
}
- (id)objectInListAtIndex:(NSUInteger)index {
if (index < [self countOfList]) {
if (index == 0) {
return [NSObject new];
}
}
return nil;
}
對于上面的例子對value進(jìn)行的兩次取值相當(dāng)于調(diào)用了兩遍 _-objectInListAtIndex: 方法,而這個方法每一次調(diào)用都會創(chuàng)建一個新的NSObject對象返回,因此element1和element2并不是同一個對象。

獲取可變集合器
獲取 key 對應(yīng)的可變集合訪問器,訪問器可以響應(yīng)對應(yīng)集合類型所有方法。該方法必定會返回一個對應(yīng)的可變集合訪問器,只是在使用時有不同的行為。
// 獲取可變數(shù)組集合訪問器
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 獲取可變有續(xù)集和訪問器
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
// 獲取可變不重復(fù)集合訪問器
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
以 MutableArray 為例(MutableOrderSet、MutableSet 同理,僅僅只是匹配的響應(yīng)方法不同,具體可以查看 api 對應(yīng)的注釋說明),獲取可變數(shù)組集合器經(jīng)過查找設(shè)置器和查找訪問器兩個步驟步驟:
1.查找設(shè)置器
1.1 查找可變集合響應(yīng)方法
對于 MutableArray 會先查找以下方法:
// 插入元素
-insertObject:in<Key>AtIndex:
// 刪除元素
-removeObjectFrom<Key>AtIndex:
// 以下方法為可選,系統(tǒng)會根據(jù)需要調(diào)用,不實現(xiàn)也不影響功能
// 批量插入,對應(yīng) -[NSMutableArray insertObjects:atIndexes:] ,
// 如果不實現(xiàn)系統(tǒng)會將批量操作拆分為單個操作,通過多次調(diào)用 -insertObject:in<Key>AtIndex: 實現(xiàn),以下方法同理
-insert<Key>:atIndexes:
// 批量移除,對應(yīng) -[NSMutableArray removeObjectsAtIndexes:]
-remove<Key>AtIndexes:
// 替換, 如果不實現(xiàn)會將操作拆分為 插入/刪除 組合
-replaceObjectIn<Key>AtIndex:withObject:
// 批量替換
-replace<Key>AtIndexes:with<Key>:
1.2 查找 key 對應(yīng)的設(shè)置器
在此步驟中,會根據(jù)與 -setValue:forKey: 方法相同的方式去查找設(shè)置器,并最終使用設(shè)置器。需要注意的是如果類中沒有對應(yīng)的設(shè)置器方法以及成員變量,最終會使用 -setValue:forKey: 作為設(shè)置器,如果此時使用可變集合訪問器去修改集合時會執(zhí)行 -setValue:forUndefinedKey: 。
2. 查找訪問器方法
在此步驟中,會根據(jù)與 -valueForKey: 方法相同的方式去查找訪問器。需要注意的是如果類中沒有對應(yīng)的訪問器方法以及成員變量,最終會使用 -valueForKey: 作為訪問器,如果此時使用可變集合訪問器去獲取集合時會執(zhí)行 -valueForUndefinedKey: 。
KVO和KVC之間有什么聯(lián)系,使用KVC賦值可以觸發(fā)KVO回調(diào)嗎?
說到KVC不得不提KVO,如果對于KVO還是不很熟悉的同學(xué)可以移步 KVO 鍵值觀察原理淺析 進(jìn)行了解。
注意:如果你還不是很清楚KVO的原理建議先了解KVO原理后再閱讀此部分內(nèi)容
KVO方法需要一個 keyPath 參數(shù),keyPath 參數(shù)雖然名為 keyPath 但是我們可以提供一個 key (eg: name) 或者 keyPath (eg: dog.name),而我們之前介紹的KVC方法也有對應(yīng)的 keyPath 存取方法,基于此我們不禁好奇KVO和KVC究竟有什么關(guān)系呢?
通過對KVO原理的了解,我們知道KVO之所以能夠監(jiān)聽某個屬性值改變,是由于其重寫了原始類相關(guān)設(shè)置器方法,并在賦值前后分別調(diào)用 -willChangeValueForKey: 和 -didChangeValueForKey: 觸發(fā)KVO監(jiān)聽回調(diào)。
類中有相關(guān)設(shè)置器方法。
KVO在查找設(shè)置器方法時的邏輯是否與KVC查找設(shè)置器方法邏輯相同呢?我們不妨寫demo來驗證一下。
下面的例子 ClassA 中擁有一個名為 -setIsName: 的設(shè)置器方法,我們通過 -setValue:forKey: 方法將 name 作為 key 賦值時會調(diào)用 -setIsName: 方法,如果我們同時將 name 作為KVO方法的 keyPath KVO回調(diào)方法會被執(zhí)行嗎?
@implementation ClassA {
NSString * _name;
}
- (instancetype)init {
if (self = [super init]) {
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
[self setValue:@"John" forKey:@"name"];
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"my name is %@", _name);
}
}
// KVC賦值第一步操作可匹配的設(shè)置器方法
- (void)setIsName:(NSString *)name {
_name = name;
}
@end
上面的例子運行起來,我們設(shè)置的KVO監(jiān)聽被觸發(fā),控制臺有如下輸出:

現(xiàn)在我們不妨查看一下KVO生成的子類 NSKVONotifying_ClassA 的方法列表:

在其中我們發(fā)現(xiàn)了 -setIsName: 這個方法,而這個方法是 ClassA 類中為 _name 提供的供KVC賦值使用的設(shè)置器方法,如此我們可以確定,如果類中提供了相關(guān)設(shè)置器方法(-set<Key>:、- _set<Key>:、- setIs<Key>:),那么當(dāng)我們設(shè)置KVO監(jiān)聽后設(shè)置器方法會被重寫,設(shè)置器方法被調(diào)用時KVO會被觸發(fā)。
類中沒有相關(guān)設(shè)置器方法。
我們將上面的例子中 ClassA 類的設(shè)置器方法代碼刪除運行,運行發(fā)現(xiàn)即使沒有相關(guān)設(shè)置器方法,KVO依然會被觸發(fā),這又是為什么呢?
我們知道KVO回調(diào)是通過 -willChangeValueForKey: 和 -didChangeValueForKey: 這兩個方法中觸發(fā)的,那么我們可以重寫 -willChangeValueForKey: 方法設(shè)置斷點來觀察一下從調(diào)用KVC方法到第一次觸發(fā)KVO回調(diào)中間的方法調(diào)用堆棧。

雖然我們無法直接查看 _NSSetValueAndNotifyForKeyInIvar 函數(shù)實現(xiàn),但是通過字面意思我們能夠大致了解,這一步直接為相關(guān)成員變量賦值并且通知回調(diào),所以這就是為什么類中沒有相關(guān)設(shè)置器方法的情況下使用KVC賦值依舊能夠觸發(fā)KVO回調(diào)的原因。
類中沒有相關(guān)設(shè)置器方法以及成員變量
將上例中 ClassA 類的設(shè)置器方法以及成員變量代碼刪除運行調(diào)用堆棧如下:

在運行上例時,我們會得到 valueForUndefinedKey: 方法拋出的異常,由此我們可以大概確定KVO內(nèi)部利用KVC來獲取舊值。需要注意的是,即使我們?yōu)轭愄砑恿伺c key 同名的方法,雖然 KVO 得以正常回調(diào),但是最后程序也會因為執(zhí)行 -setValue:forUndefinedKey: 崩潰。
實際上通過對KVC賦值邏輯三個步驟以及在每一種情況下的調(diào)用堆棧觀察,我們可以得出以下結(jié)論:
查找到相關(guān)設(shè)置器方法:
setValue:forKey: -> _NSSetObjectValueAndNotify -> willChangeValueForKey:查找到相關(guān)成員變量:
setValue:forKey: -> _NSSetValueAndNotifyForKeyInIvar -> willChangeValueForKey:沒有查找到相關(guān)設(shè)置器方法以及成員變量:
setValue:forKey: -> _NSSetValueAndNotifyForUndefinedKey -> willChangeValueForKey:
也就是說KVC設(shè)置器方法實際上會根據(jù)每一種情況提供對KVO的處理,所以我們在設(shè)置了KVO監(jiān)聽之后使用KVC賦值是可以觸發(fā)KVO回調(diào)的。
那么KVC在什么情況下會去處理KVO監(jiān)聽呢? 不知大家是否還記得我們在之前查看KVO監(jiān)聽后生成的子類方法列表時,其中有一個特殊的方法 _isKVOA,當(dāng)時我們并沒有提到它,那么現(xiàn)在它的作用不言自明。
KVO 監(jiān)聽與可變集合訪問器
上文中我們說到了獲取可變集合訪問器方法,那么如果對一個 key 設(shè)置了 KVO 監(jiān)聽,我們在使用可變集合訪問器去修改集合的時候,KVO 回調(diào)會被觸發(fā)嗎?
答案是取決于可變集合訪問器獲取的時機
如下的例子
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"arr"]) {
NSLog(@"arr改變了 %@", change);
}
}
// 添加 KVO 前獲取可變集合訪問器
NSMutableArray * mutableArr1 = [t mutableArrayValueForKey:@"arr"];
// 添加 KVO 監(jiān)聽
[t addObserver:self forKeyPath:@"arr" options:NSKeyValueObservingOptionNew context:nil];
// 添加 KVO 之后獲取可變集合訪問器
NSMutableArray * mutableArr2 = [t mutableArrayValueForKey:@"arr"];
// 初始化 t 的 arr 值,觸發(fā) KVO 回調(diào)
[t setValue:[NSMutableArray array] forKey:@"arr"];
// 通過添加 KVO 前獲取的可變集合訪問器添加元素,不會觸發(fā) KVO 回調(diào)
[mutableArr1 addObject:@"1"];
// 通過添加 KVO 后獲取的可變集合訪問器添加元素,觸發(fā) KVO 回調(diào)
[mutableArr2 addObject:@"2"];
輸出結(jié)果如下:
2023-12-09 11:17:34.752834+0800 Demo1[34580:3276019] arr改變了 {
kind = 1;
new = (
);
}
2023-12-09 11:17:54.928467+0800 Demo1[34580:3276019] arr改變了 {
indexes = "<_NSCachedIndexSet: 0x280a343c0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
2
);
}
根據(jù)結(jié)果可知,在添加 KVO 前獲取的可變集合訪問器修改元素不會觸發(fā) KVO,而之后獲取的可以觸發(fā) KVO 且 KVO 回調(diào)中 change 包含改變的元素和改變的元素下標(biāo)。