iOS - 關于 KVC 的一些總結

KVC 大綱

目錄

  • 1. 什么是 KVC
  • 2. 訪問對象屬性
  • 3. 訪問集合屬性
  • 4. 使用集合運算符
  • 5. 自定義集合運算符
  • 6. 非對象值處理
  • 7. 屬性驗證
  • 8. 搜索規則
  • 9. 異常處理
  • 10. 相關面試題
  • 參考

1. 什么是 KVC

  • KVC的全稱是Key-Value Coding(鍵值編碼),是由NSKeyValueCoding非正式協議啟用的一種機制,對象采用這種機制來提供對其屬性的間接訪問,可以通過字符串來訪問一個對象的成員變量或其關聯的存取方法(getter or setter)。
  • 通常,我們可以直接通過存取方法或變量名來訪問對象的屬性。我們也可以使用KVC間接訪問對象的屬性,并且KVC還可以訪問私有變量。某些情況下,KVC還可以幫助簡化代碼。
  • KVC是許多其他 Cocoa 技術的基礎概念,比如 KVO、Cocoa bindings、Core Data、AppleScript-ability 等等。

2. 訪問對象屬性

常用 API

- (nullable id)valueForKey:(NSString *)key;         // 通過 key 來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通過 keyPath 來取值

- (void)setValue:(nullable id)value forKey:(NSString *)key;         // 通過 key 來賦值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通過 keyPath 來賦值

基礎操作

如下是 BankAccount 類的聲明:

@interface BankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
@end

對于 BankAccount 的實例對象myAccount
我們可以使用setter方法為currentBalance屬性賦值,這是直接的,但缺乏靈活性。

[myAccount setCurrentBalance:@(100.0)];

我們也可以通過KVC間接為currentBalance屬性賦值,通過其鍵Key設置值。

[myAccount setValue:@(100.0) forKey:@"currentBalance"];

KeyPath

KVC還支持多級訪問,KeyPath用法跟點語法相同。
例如:我們想對myAccountowner屬性的address屬性的street屬性賦值,其KeyPathowner.address.street。

[myAccount setValue:@"地址" forKeyPath:@"owner.address.street"];

多值操作

給定一組Key,獲得一組value,以字典的形式返回。該方法為數組中的每個Key調用valueForKey:方法。

- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

將指定字典中的值設置到消息接收者的屬性中,使用字典的Key標識屬性。默認實現是為每個鍵值對調用setValue:forKey:方法 ,會根據需要用nil替換NSNull對象。

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;

3. 訪問集合屬性

我們可以像訪問其它對象一樣使用valueForKey:setValue:forKey:方法來獲取或設置集合對象(主要指NSArrayNSSet)。但是,當我們要操作集合對象的內容,比如添加或者刪除元素時,通過KVC的可變代理方法獲取集合代理對象是最有效的。
根據KVO的實現原理,是在運行時動態生成子類并重寫setter方法來達到可以通知所有觀察者對象的目的,因此我們對集合對象進行操作是不會觸發KVO的。當我們要使用KVO監聽集合對象變化時,需要通過KVC的可變代理方法獲取集合代理對象,然后對代理對象進行操作。當代理對象的內部對象發生改變時,會觸發KVO的監聽方法。
傳送門:iOS - 關于 KVO 的一些總結

KVC提供了三種不同的代理對象訪問的代理方法,每種都有KeyKeyPath兩種方法。

  • mutableArrayValueForKey:mutableArrayValueForKeyPath:

    返回NSMutableArray對象的代理對象。

  • mutableSetValueForKey:mutableSetValueForKeyPath:

    返回NSMutableSet對象的代理對象。

  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:

    返回NSMutableOrderedSet對象的代理對象。

4. 使用集合運算符

KVCvalueForKeyPath:方法除了可以取出屬性值以外,還可以在KeyPath中嵌套集合運算符,來對集合對象進行操作。

以下是KeyPath集合運算符的格式,主要分為 3 個部分,如圖 4-1。

  • Left key path:左鍵路徑,要操作的集合對象,如果消息接收者就是集合對象,則可以省略 Left 部分;
  • Collection operator:集合運算符;
  • Right key path:右鍵路徑,要進行運算的集合中的屬性。
圖 4-1 KeyPath 集合運算符格式.png

集合運算符主要分為三類:

  • ① 聚合運算符:以某種方式合并集合中的對象,并返回右鍵路徑中指定的屬性的數據類型匹配的一個對象,一般返回NSNumber實例。
  • ② 數組運算符:根據運算符的條件,將符合條件的對象以一個NSArray實例返回。
  • ③ 嵌套運算符:處理集合對象中嵌套其他集合對象的情況,并根據運算符返回一個NSArrayNSSet實例。

示例

如下是 BankAccount 類和 Transaction 類的聲明。BankAccount 中有一個 transactions 數組屬性,其元素為 Transaction 類型。Transaction 類中定義了 3 個屬性,分別為收款人、金額、日期。

@interface BankAccount : NSObject
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many 
@end

@interface Transaction : NSObject
@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When
@end

下表是為了演示集合運算符使用而給出的 transactions 數組的數據。

payee amount date
Green Power $120.00 Dec 1, 2015
Green Power $150.00 Jan 1, 2016
Green Power $170.00 Feb 1, 2016
Car Loan $250.00 Jan 15, 2016
Car Loan $250.00 Feb 15, 2016
Car Loan $250.00 Mar 15, 2016
General Cable $120.00 Dec 1, 2015
General Cable $155.00 Jan 1, 2016
General Cable $120.00 Feb 1, 2016
Mortgage $1,250.00 Jan 15, 2016
Mortgage $1,250.00 Feb 15, 2016
Mortgage $1,250.00 Mar 15, 2016
Animal Hospital $600.00 Jul 15, 2016

聚合運算符

以某種方式合并集合中的對象,并返回右鍵路徑中指定的屬性的數據類型匹配的一個對象,一般返回NSNumber實例。

@avg

讀取集合中每個元素的右鍵路徑指定的屬性,將其轉換為double類型 (nil用 0 替代),并計算這些值的算術平均值。然后將結果以NSNumber實例返回。

// 計算上表中 amount 的平均值。
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
// transactionAverage 格式化的結果為 $ 456.54。

@count

計算集合中的元素個數,以NSNumber實例返回。

// 計算 transactions 集合中的元素個數。
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
// numberOfTransactions 的值為 13。

備注: @count運算符比較特別,它不需要寫右鍵路徑,即使寫了也會被忽略。

@sum

讀取集合中每個元素的右鍵路徑指定的屬性,將其轉換為double類型 (nil用 0 替代),并計算這些值的總和。然后將結果以NSNumber實例返回。

// 計算上表中 amount 的總和。
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
// amountSum 的結果為 $ 5935.00。

@max

返回集合中右鍵路徑指定的屬性的最大值。

// 獲取日期的最大值。
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
// latestDate 的值為 Jul 15, 2016.

@min

返回集合中右鍵路徑指定的屬性的最小值。

// 獲取日期的最小值。
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
// earliestDate 的值為 Dec 1, 2015.

備注: @max@min根據右鍵路徑指定的屬性在集合中搜索,搜索使用compare:方法進行比較,許多基礎類 (如NSNumber類) 中都有定義。因此,右鍵路徑指定的屬性必須能響應compare:消息。搜索忽略值為nil的集合項??梢酝ㄟ^重寫compare:方法對搜索過程進行控制。

數組運算符

根據運算符的條件,將符合條件的對象以一個NSArray實例返回。

@unionOfObjects

讀取集合中每個元素的右鍵路徑指定的屬性,放在一個NSArray實例中并返回。

// 獲取集合中的所有 payee 對象。
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
// payees 數組包含以下字符串:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital。

@distinctUnionOfObjects

讀取集合中每個元素的右鍵路徑指定的屬性,放在一個NSArray實例中,將數組進行去重后返回。

// 獲取集合中的所有不同的 payee 對象。
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
// distinctPayees 數組包含以下字符串:Car Loan, General Cable, Animal Hospital, Green Power, Mortgage。

注意: 在使用數組運算符時,如果有任何操作的對象為nil,則valueForKeyPath:方法將引發異常。

嵌套運算符

處理集合對象中嵌套其他集合對象的情況,并根據運算符返回一個NSArrayNSSet實例。

如下 moreTransactions 是裝著 transaction 對象的數組,arrayOfArrays 數組中嵌套了 self.transactions 和 moreTransactions 兩個數組。

NSArray* moreTransactions = @[<# transaction data #>];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];

下表是 moreTransactions 數組的數據。

payee amount date
General Cable - Cottage $120.00 Dec 18, 2015
General Cable - Cottage $155.00 Jan 9, 2016
General Cable - Cottage $120.00 Dec 1, 2016
Second Mortgage $1,250.00 Nov 15, 2016
Second Mortgage $1,250.00 Sep 20, 2016
Second Mortgage $1,250.00 Feb 12, 2016
Hobby Shop $600.00 Jun 14, 2016

@unionOfArrays

讀取集合中的每個集合中的每個元素的右鍵路徑指定的屬性,放在一個NSArray實例中并返回。

// 獲取 arrayOfArrays 集合中的每個集合中的所有 payee 對象。
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
// collectedPayees 數組包含以下字符串:Green Power, Green Power, Green Power, Car Loan, Car Loan, Car Loan, General Cable, General Cable, General Cable, Mortgage, Mortgage, Mortgage, Animal Hospital, General Cable - Cottage, General Cable - Cottage, General Cable - Cottage, Second Mortgage, Second Mortgage, Second Mortgage, Hobby Shop.

@distinctUnionOfArrays

讀取集合中的每個集合中的每個元素的右鍵路徑指定的屬性,放在一個NSArray實例中,將數組進行去重后返回。

// 獲取 arrayOfArrays 集合中的每個集合中的所有不同的 payee 對象。
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
// collectedDistinctPayees 數組包含以下字符串:Hobby Shop, Mortgage, Animal Hospital, Second Mortgage, Car Loan, General Cable - Cottage, General Cable, Green Power.

@distinctUnionOfSets

讀取集合中的每個集合中的每個元素的右鍵路徑指定的屬性,放在一個NSSet實例中,去重后返回。

NSSet *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfSets.payee"];

注意:

  • 在使用嵌套運算符時,valueForKeyPath:內部會根據運算符創建一個NSMutableArrayNSMutableSet對象,將集合中的arrayset添加進去再進行操作。如果集合中有非集合元素,會導致Crash。
  • 使用unionOfArraysdistinctUnionOfArrays運算符,消息接收者應該是arrayOfArrays類型,即NSArray< NSArray* >* arrayOfArrays;;使用distinctUnionOfSets運算符,消息接收者應該是setOfSets或者arrayOfSets類型。否則會發生異常。
  • 在使用嵌套運算符時,如果有任何操作的對象為nil, 則valueForKeyPath:方法將引發異常。

拓展

如果集合中的對象都是NSNumber,右鍵路徑可以用self。

    NSArray *array = @[@1, @2, @3, @4, @5];
    NSNumber *sum = [array valueForKeyPath:@"@sum.self"];
    NSLog(@"%d",[sum intValue]); 

5. 自定義集合運算符

上面介紹了KVC為我們提供的集合運算符,我們能不能自定義呢?

我們使用Runtime打印NSArray類的方法列表:

- (void)printNSArrayMethods
{
    u_int count;
    Method *methods = class_copyMethodList([NSArray class], &count);
    for (int i = 0; i < count ; i++)
    {
        Method method = methods[i];
        SEL sel = method_getName(method);
        NSLog(@"%d---%@", i, NSStringFromSelector(sel));
    }
    free(methods);
}
0---mr_isEqualToOutputDevicesArray:
1---mr_containsAnyOf:
2---mr_map:
3---sg_enumerateChunksOfSize:usingBlock:
4---_pas_mappedArrayWithTransform:
5---_pas_shuffledArrayUsingRng:
......

方法很多,我們搜索關鍵字avg、count、sumKVC為我們提供的集合運算符,發現都有對應的方法_<operatorKey>ForKeyPath:

267---_avgForKeyPath:
268---_countForKeyPath:
264---_sumForKeyPath:
269---_maxForKeyPath:
270---_minForKeyPath:
266---_unionOfObjectsForKeyPath:
273---_distinctUnionOfObjectsForKeyPath:
265---_unionOfArraysForKeyPath:
272---_distinctUnionOfArraysForKeyPath:
274---_distinctUnionOfSetsForKeyPath:

注意: 我們再來看一下NSSet類支持哪些集合運算符:

50---_sumForKeyPath:
51---_avgForKeyPath:
52---_countForKeyPath:
53---_maxForKeyPath:
54---_minForKeyPath:
55---_distinctUnionOfArraysForKeyPath:
56---_distinctUnionOfObjectsForKeyPath:
57---_distinctUnionOfSetsForKeyPath:

可見NSSet類不支持@unionOfObjects@unionOfArrays運算符,如果使用了就會拋出異常NSInvalidArgumentException并導致程序崩潰,reason: [<__NSSetI 0x6000017a12f0> valueForKeyPath:]: this class does not implement the unionOfArrays operation.不支持該運算符。

NSArray類雖然支持@distinctUnionOfSets運算符,但其必須是arrayOfSets類型,即NSArray< NSSet* >* arrayOfSets;。因為_distinctUnionOfSetsForKeyPath方法中會創建一個NSMutableSet實例,并調用unionSet:方法將集合中的set的元素添加進去再進行操作。如果是arrayOfArrays類型就會拋出異常NSInvalidArgumentException并導致程序崩潰,reason: '*** -[NSMutableSet unionSet:]: set argument is not an NSSet'即集合中有非NSSet元素。

我們嘗試為NSArray添加一個分類,并定義一個_medianForKeyPath:方法,用來獲取NSArray中的中位數。

#import <Foundation/Foundation.h>
@interface NSArray (HTOperator)
- (NSNumber *)_medianForKeyPath:(NSString *)keyPath;
@end

#import "NSArray+HTOperator.h"
@implementation NSArray (HTOperator)
- (NSNumber *)_medianForKeyPath:(NSString *)keyPath {
    //排序
    NSArray *sortedArray = [self sortedArrayUsingSelector:@selector(compare:)];
    double median;
    if (self.count % 2 == 0) {
        NSInteger index1 = sortedArray.count * 0.5;
        NSInteger index2 = sortedArray.count * 0.5 - 1;
        median = ([[sortedArray objectAtIndex:index1] doubleValue] + [[sortedArray objectAtIndex:index2] doubleValue]) * 0.5;        
    } else {
        NSInteger index = (sortedArray.count-1) * 0.5;
        median = [[sortedArray objectAtIndex:index] doubleValue];
    }
    return [NSNumber numberWithDouble:median];
}

測試。

    NSArray *array = @[@9, @7, @8, @2, @6, @3];
    NSNumber *num = [array valueForKeyPath:@"@median.self"];
    NSLog(@"%f",[num doubleValue]);
    // 6.500000

6. 非對象值處理

KVC支持基礎數據類型和結構體,在使用KVC進行賦值或取值的時候,會自動在非對象值和對象值之間進行轉換。

  • 當進行取值如valueForKey:時,如果返回值非對象,會使用該值初始化一個NSNumber(用于基礎數據類型)或NSValue(用于結構體)實例,然后返回該實例。
  • 當進行賦值如setValue:forKey:時,如果key的數據類型非對象,則會發送一條<type>Value消息給value對象以提取基礎數據,然后賦值給key

注意:

  • 因為Swift中的所有屬性都是對象,所以這里僅適用于Objective-C屬性。
  • 當進行賦值如setValue:forKey:時,如果key的數據類型是非對象類型,則value就禁止傳nil。否則會調用setNilValueForKey:方法,該方法的默認實現拋出異常NSInvalidArgumentException,并導致程序Crash。

下表是KVC對于基礎數據類型和NSNumber對象之間的轉換。

Data type Creation method Accessor method
BOOL numberWithBool: boolValue (in iOS)
charValue (in macOS)*
char numberWithChar: charValue
double numberWithDouble: doubleValue
float numberWithFloat: floatValue
int numberWithInt: intValue
long numberWithLong: longValue
long long numberWithLongLong: longLongValue
short numberWithShort: shortValue
unsigned char numberWithUnsignedChar: unsignedChar
unsigned int numberWithUnsignedInt: unsignedInt
unsigned long numberWithUnsignedLong: unsignedLong
unsigned long long numberWithUnsignedLongLong: unsignedLongLong
unsigned short numberWithUnsignedShort: unsignedShort

下表是KVC對于結構體類型和NSValue對象之間的轉換。

Data type Creation method Accessor method
CGPoint valueWithCGPoint: CGPointValue
CGRect valueWithCGRect: CGRectValue
CGSize valueWithCGSize: CGSizeValue
NSRange valueWithRange: rangeValue

除了以上CGPointCGRectCGSizeNSRange類型的結構體可以和NSValue對象之間進行轉換,我們自定義的結構體也可以包裝成NSValue對象,示例如下。

typedef struct {
    float x, y, z;
} ThreeFloats;
 
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
// 取值
NSValue* result = [myClass valueForKey:@"threeFloats"];
// 賦值
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];

7. 屬性驗證

KVC提供了屬性驗證的方法,如下。我們可以在使用KVC賦值前驗證能否為這個key賦值指定value。
validateValue方法的默認實現是查看消息接收者類中是否實現了遵循命名規則為validate<Key>:error:的方法,如果有的話就返回調用該方法的結果;如果沒有的話,則默認驗證成功并返回YES。我們可以在消息接收者類中實現validate<Key>:error:的方法來自定義邏輯返回YESNO。

- (BOOL)validateValue:(id  _Nullable *)value 
               forKey:(NSString *)key 
                error:(NSError * _Nullable *)error;

- (BOOL)validateValue:(inout id  _Nullable *)ioValue 
           forKeyPath:(NSString *)inKeyPath 
                error:(out NSError * _Nullable *)outError;

示例
Person類中實現了validateName:error:方法,驗證給name賦的值是不是jack。

// ViewController.m
    Person *person = [[Person alloc] init];
    NSString *value = @"rose";
    NSString *key = @"name";
    NSError  *error;
    BOOL result = [person validateValue:&value forKey:key error:&error];
    
    if (error) {
        NSLog(@"error = %@", error);
        return;
    }
    NSLog(@"%d",result);

// Person.m
- (BOOL)validateName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
{
    NSString *name = *value;
    BOOL result = NO;
    if ([name isEqualToString:@"jack"]) {
        result = YES;
    }
    return result;
}
// 打印:0

備注: 默認情況下,KVC是不會自動驗證屬性的。

8. 搜索規則

除了了解KVC的使用,了解KVC取值和賦值過程的工作原理也是很有必要的。

基本的 Getter 搜索模式

以下是valueForKey:方法的默認實現,給定一個key作為輸入參數,在消息接收者類中操作,執行以下過程。

  • ① 按照get<Key>、<key>、is<Key>、_<key>順序查找方法。
    如果找到就調用取值并執行⑤,否則執行②;
  • ② 查找countOf<Key>objectIn<Key>AtIndex:、<key>AtIndexes:命名的方法。
    如果找到第一個和后面兩個中的至少一個,則創建一個能夠響應所有NSArray的方法的集合代理對象(類型為NSKeyValueArray,繼承自NSArray),并返回該對象。否則執行③;
    • 代理對象隨后將其接收到的任何NSArray消息轉換為countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes:消息的組合,并將其發送給KVC調用方。如果原始對象還實現了一個名為get<Key>:range:的可選方法,則代理對象也會在適當時使用該方法。
    • KVC調用方與代理對象一起工作時,允許底層屬性的行為如同NSArray一樣,即使它不是NSArray。
  • ③ 查找countOf<Key>、enumeratorOf<Key>memberOf<Key>:命名的方法。
    如果三個方法都找到,則創建一個能夠響應所有NSSet的方法的集合代理對象(類型為NSKeyValueSet,繼承自NSSet),并返回該對象。否則執行④;
    • 代理對象隨后將其接收到的任何NSSet消息轉換為countOf<Key>、enumeratorOf<Key>、memberOf<Key>:消息的組合,并將其發送給KVC調用方。
    • KVC調用方與代理對象一起工作時,允許底層屬性的行為如同NSSet一樣,即使它不是NSSet。
  • ④ 查看消息接收者類的+accessInstanceVariablesDirectly方法的返回值(默認返回YES)。如果返回YES,就按照_<key>_is<Key>、<key>、is<Key>順序查找成員變量。如果找到就直接取值并執行⑤,否則執行⑥。如果+accessInstanceVariablesDirectly方法返回NO也執行⑥。
  • ⑤ 如果取到的值是一個對象指針,即獲取的是對象,則直接將對象返回。
    ? 如果取到的值是一個NSNumber支持的數據類型,則將其存儲在NSNumber實例并返回。
    ? 如果取到的值不是一個NSNumber支持的數據類型,則轉換為NSValue對象, 然后返回。
  • ⑥ 調用valueForUndefinedKey:方法,該方法拋出異常NSUnknownKeyException,并導致程序Crash。這是默認實現,我們可以重寫該方法根據特定key做一些特殊處理。

基本的 Setter 搜索模式

以下是setValue:forKey:方法的默認實現,給定keyvalue作為輸入參數,嘗試將KVC調用方的屬性名為key的值設置為value,執行以下過程。

  • ① 按照set<Key>:_set<Key>:順序查找方法。
    如果找到就調用并將value傳進去(根據需要進行數據類型轉換),否則執行②。
  • ② 查看消息接收者類的+accessInstanceVariablesDirectly方法的返回值(默認返回YES)。如果返回YES,就按照_<key>、_is<Key>、<key>is<Key>順序查找成員變量(同 基本的Getter搜索模式)。如果找到就將value賦值給它(根據需要進行數據類型轉換),否則執行③。如果+accessInstanceVariablesDirectly方法返回NO也執行③。
  • ③ 調用setValue:forUndefinedKey:方法,該方法拋出異常NSUnknownKeyException,并導致程序Crash。這是默認實現,我們可以重寫該方法根據特定key做一些特殊處理。

NSMutableArray 搜索模式

以下是mutableArrayValueForKey:方法的默認實現,給定一個key作為輸入參數,返回屬性名為key的集合的代理對象(這里指NSMutableArray對象),在消息接收者類中操作,執行以下過程。

  • ① 查找一對方法insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex:
    (相當于NSMutableArray的原始方法insertObject:atIndex:removeObjectAtIndex:),
    或者insert<Key>:atIndexes:remove<Key>AtIndexes:
    (相當于NSMutableArray的原始方法insertObjects:atIndexes:removeObjectsAtIndexes:)。

    • 如果我們至少實現了一個insertion方法和一個removal方法,則返回一個代理對象,來響應發送給NSMutableArray的消息,通過發送insertObject:in<Key>AtIndex:、removeObjectFrom<Key>AtIndex:、insert<Key>:atIndexes:、remove<Key>AtIndexes:組合消息給KVC調用方。否則執行②。

    該代理對象類型為NSKeyValueFastMutableArray2,繼承鏈為NSKeyValueFastMutableArray2->NSKeyValueFastMutableArray->NSKeyValueMutableArray->NSMutableArray。

    • 如果我們也實現了一個可選的replace object方法,如replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:,代理對象在適當的情況下也會使用它們,以獲得最佳性能。
  • ② 查找set<Key>:方法。
    如果找到,就會向KVC調用方發送一個set<Key>:消息,來返回一個響應NSMutableArray消息的代理對象。否則執行③。

    該代理對象類型為NSKeyValueSlowMutableArray,繼承鏈為NSKeyValueSlowMutableArray->NSKeyValueMutableArray->NSMutableArray。

注意:
此步驟中描述的機制比上一步的效率低得多,因為它可能重復創建新的集合對象,而不是修改現有的集合對象。因此,在設計自己的鍵值編碼兼容對象時,通常應該避免使用它。
給代理對象發送NSMutableArray消息都會調用set<Key>:方法。即,對代理對象進行修改,都是調用set<Key>:來重新賦值,所以效率會低很多。

  • ③ 查看消息接收者類的+accessInstanceVariablesDirectly方法的返回值(默認返回YES)。如果返回YES,就按照_<key>、<key>順序查找成員變量。如果找到就返回一個代理對象,該代理對象將接收所有NSMutableArray消息,通常是NSMutableArray或其子類。否則執行④。如果+accessInstanceVariablesDirectly方法返回NO也執行④。
  • ④ 返回一個可變的集合代理對象。當它接收到NSMutableArray消息時,發送一個valueForUndefinedKey:消息給KVC調用方,該方法拋出異常NSUnknownKeyException,并導致程序Crash。這是默認實現,我們可以重寫該方法根據特定key做一些特殊處理。

其他搜索模式

除了以上三種,KVC還有NSMutableSetNSMutableOrderedSet兩種搜索模式,它們的搜索規則和NSMutableArray相同,只是搜索和調用的方法不同。具體可以查看KVC官方文檔 KVC - Accessor Search Patterns。

9. 異常處理

  • ① 根據KVC搜索規則,當沒有搜索到對應的key或者keyPath相關方法或者變量時,會調用對應的異常方法valueForUndefinedKey:setValue:forUndefinedKey:,這兩個方法的默認實現是拋出異常NSUnknownKeyException,并導致程序Crash。我們可以重寫這兩個方法來處理異常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
  • ② 當進行賦值如setValue:forKey:時,如果key的數據類型是非對象類型,則value就禁止傳nil。否則會調用setNilValueForKey:方法,該方法的默認實現是拋出異常NSInvalidArgumentException,并導致程序Crash。我們可以重寫這個方法來處理異常。
- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"hidden"]) {
        [self setValue:@(NO) forKey:@”hidden”];
    } else {
        [super setNilValueForKey:key];
    }
}

10. 相關面試題

Q:通過 KVC 修改屬性會觸發 KVO 嗎?

會,通過KVC修改成員變量值也會觸發KVO

Q:通過 KVC 鍵值編碼技術是否會破壞面向對象的編程方法,或者說違背面向對象的編程思想呢?

valueForKey:setValue:forKey:這里面的key是沒有任何限制的,當我們知道一個類或實例它內部的私有變量名稱的情況下,我們在外界可以通過已知的key來對它的私有變量進行訪問或者賦值的操作,從這個角度來講KVC鍵值編碼技術會違背面向對象的編程思想。

參考

Key-Value Coding Programming Guide(蘋果官方文檔)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 原文:iOS 關于KVC的一些總結 本文參考: KVC官方文檔 KVC原理剖析 iOS KVC詳解 KVC 簡介 ...
    liyoucheng2014閱讀 954評論 0 3
  • 本文參考: KVC官方文檔 KVC原理剖析 iOS KVC詳解 KVC 簡介 KVC全稱是Key Value Co...
    擰發條鳥xds閱讀 5,318評論 6 23
  • KVC(Key-valuecoding)鍵值編碼,單看這個名字可能不太好理解。其實翻譯一下就很簡單了,就是指iOS...
    榕樹頭閱讀 723評論 0 2
  • 什么是KVC? KVC(Key-value coding)鍵值編碼,單看這個名字可能不太好理解。其實是指iOS的開...
    祀夢_閱讀 948評論 0 7
  • 今天戴老師留了一項特殊的作業,是什么呢?畫畫!戴老師讓我們畫一副有關《江南》的畫。 回到家,我寫完了作業,拿了一張...
    陳泉妡閱讀 295評論 0 1