iOS KVC賦值原理

在開發(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 集合代理對象
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)致程序崩潰。

數(shù)組下標(biā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并不是同一個對象。

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ā),控制臺有如下輸出:

KVC與KVO

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

在其中我們發(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)用堆棧。

圖1.png

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

類中沒有相關(guān)設(shè)置器方法以及成員變量

將上例中 ClassA 類的設(shè)置器方法以及成員變量代碼刪除運行調(diào)用堆棧如下:

圖2.png

在運行上例時,我們會得到 valueForUndefinedKey: 方法拋出的異常,由此我們可以大概確定KVO內(nèi)部利用KVC來獲取舊值。需要注意的是,即使我們?yōu)轭愄砑恿伺c key 同名的方法,雖然 KVO 得以正常回調(diào),但是最后程序也會因為執(zhí)行 -setValue:forUndefinedKey: 崩潰。


實際上通過對KVC賦值邏輯三個步驟以及在每一種情況下的調(diào)用堆棧觀察,我們可以得出以下結(jié)論:

  1. 查找到相關(guān)設(shè)置器方法:
    setValue:forKey: -> _NSSetObjectValueAndNotify -> willChangeValueForKey:

  2. 查找到相關(guān)成員變量:
    setValue:forKey: -> _NSSetValueAndNotifyForKeyInIvar -> willChangeValueForKey:

  3. 沒有查找到相關(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)。


文章中如果有任何問題,或者講述不是很容易理解的地方,大家可以私信我或者在評論區(qū)提出,后續(xù)我會根據(jù)大家的反饋對文章進(jìn)行補充修改。

感謝?。?!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • iOS 底層原理 文章匯總[http://www.itdecent.cn/p/412b20d9a0f6] KVC...
    Style_月月閱讀 3,636評論 0 21
  • 不管是平常開發(fā)還是找工作面試中,KVC、KVO的原理都是面試官比較喜歡問的問題。最近抽時間研究了一下KVC和KVO...
    coolLee閱讀 1,267評論 0 2
  • KVC 1.簡介 KVC全稱是Key Value Coding(鍵值編碼),是可以通過對象屬性名稱(Key)直接給...
    Jt_Self閱讀 618評論 0 0
  • KVC(Key-value coding)鍵值編碼,單看這個名字可能不太好理解。其實翻譯一下就很簡單了,就是指iO...
    朽木自雕也閱讀 1,698評論 6 1
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 7,646評論 0 4

友情鏈接更多精彩內(nèi)容