什么是KVC?
KVC(Key-value coding)鍵值編碼,單看這個名字可能不太好理解。其實是指iOS的開發中,可以允許開發者通過Key名直接訪問對象的屬性,或者給對象的屬性賦值。這樣就可以在運行時動態地訪問和修改對象的屬性。而不是在編譯時確定,很多高級的iOS開發技巧都是基于KVC實現的。目前網上關于KVC的文章在非常多,有的只是簡單地說了下用法,我會運用圖解的方式寫下這遍文章就是為了讓大家更好的理解。
KVC方法全覽
KVC
提供了一種間接訪問其屬性方法或成員變量的機制,可以通過字符串來訪問對應的屬性方法或成員變量。
KVC基礎操作
KVC取值
取值方法
- 通過key
- (nullable id)valueForKey:(NSString *)key; //直接通過Key來取值
- 通過keyPath
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通過KeyPath來取值
基于getter取值底層實現
當調用
valueForKey
的代碼時,其搜索方式如下:你需要先看一下這張流程圖,大致知道如何運轉的,之后再看文字描述,仔細了解其機制
通過
getter
方法搜索實例,按照get<Key>
,<key>
,is<Key>
,_<key>的順序查找
getter`方法。如果發現符合的方法,就調用對應的方法并拿著結果跳轉到第五步。否則,就繼續到下一步。-
如果沒有找到簡單的
getter
方法,則搜索其匹配模式的方法countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
。如果找到其中的第一個和其他兩個中的一個,則就會返回一個可以響應
NSArray
所有方法的代理集合(它是NSKeyValueArray
,是NSArray
的子類)。或者說給這個代理集合發送屬于NSArray
的方法,就會以countOf<Key>
,objectIn<Key>AtIndex
或<Key>AtIndexes
這幾個方法組合的形式調用。否則,繼續到第三步。代理對象隨后將
NSArray
接收到的countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
的消息給符合KVC
規則的調用方。當代理對象和
KVC
調用方通過上面方法一起工作時,就會允許其行為類似于NSArray
一樣。 -
如果沒有找到
NSArray
簡單存取方法,或者NSArray
存取方法組。那么會同時查找countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
命名的方法。如果找到三個方法,則創建一個集合代理對象,該對象響應所有
NSSet
方法并返回。否則,繼續執行第四步。給這個代理對象發
NSSet
的消息,就會以countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>
組合的形式調用。 -
如果沒有發現簡單
getter
方法,或集合存取方法組,以及接收類方法accessInstanceVariablesDirectly
是返回YES
的。搜索一個名為_<key>
、_is<Key>
、<key>
、is<Key>
的實例,根據他們的順序。如果發現對應的實例,則立刻獲得實例可用的值并跳轉到第五步,如果重寫了類方法
+ (BOOL)accessInstanceVariablesDirectly
返回NO的話,那么會直接調用valueForUndefinedKey:
。 如果取回的是一個對象指針,則直接返回這個結果。
如果取回的是一個基礎數據類型,但是這個基礎數據類型是被NSNumber
支持的,則存儲為NSNumber
并返回。
如果取回的是一個不支持NSNumber
的基礎數據類型,則通過NSValue
進行存儲并返回。如果所有情況都失敗,則調用
valueForUndefinedKey:
方法并拋出異常,這是默認行為。但是子類可以重寫此方法。
KVC設值
賦值方法
- 通過key
-
直接將屬性名當做
key
,并設置value
,即可對屬性進行賦值。- (void)setValue:(nullable id)value forKey:(NSString *)key; //通過Key來設值
- 通過keyPath
-
除了對當前對象的屬性進行賦值外,還可以對其更“深層”的對象進行賦值。
KVC
進行多級訪問時,直接類似于屬性調用一樣用點語法進行訪問即可。例如Person
屬性中有name
屬性,我就可以通過Person.name
進行賦值- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通過KeyPath來設值
基于setter賦值底層實現
這是
setValue:forKey:
的默認實現,給定輸入參數value
和key
。試圖在接收調用對象的內部,設置屬性名為key
的value
,通過下面的步驟你需要先看一下這張流程圖,大致知道如何運轉的,之后再看文字描述,仔細了解其機制
- 查找
set<Key>:
或_set<Key>
命名的setter
,按照這個順序,如果找到的話,代碼通過setter
方法完成設置。 - 如果沒有找到
setter
方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly
的返回值,如果accessInstanceVariablesDirectly
類屬性返回YES
,則查找一個命名規則為_<key>
、_is<Key>
、<key>
、is<Key>
的實例變量。根據這個順序,如果發現則將value
賦值給實例變量,如果返回值為NO,KVC會執行setValue:forUndefinedKey:
方法。 - 如果沒有發現
setter
或實例變量,則調用setValue:forUndefinedKey:
方法,并默認提出一個異常,但是一個NSObject
的子類可以提出合適的行為。
KVC批量操作
-
在對象調用
setValuesForKeysWithDictionary:
方法時,可以傳入一個包含key
、value
的字典進去,KVC
可以將所有數據按照屬性名和字典的key
進行匹配,并將value
給User
對象的屬性賦值。//創建一個model模型,里面的字符串名稱必須和key的名稱對應,不然該方法會崩潰 @interface PersonModel : NSObject @property (nonatomic, copy) NSString *key1; @property (nonatomic, copy) NSString *key2; @property (nonatomic, copy) NSString *id; @property (nonatomic, copy) NSString *key3; @property (nonatomic, copy) NSString *other; @end PersonModel *person = [[PersonModel alloc] init]; //1.這是直接賦值,數據量小會很簡單,但是數據量一多就很麻煩,就像我們進行網絡請求時 person.key1 = dictionary[@"key1"]; person.key2 = dictionary[@"key2"]; person.key3 = dictionary[@"key3"]; //2.通過下面該方法可以批量賦值 //2.1如果model里面的string不存在于dictionary中,輸出結果為null; [person setValuesForKeysWithDictionary:dictionary]; NSLog(@"\n%@\n%@\n%@\n%@\n", person.key1,person.key2,person.key3,person.other); //輸出結果 test1 test2 test3 (null) //2.2如果dictionary中有的元素,moedl中沒有運行會直接出錯,那么我們應該怎么解決? //我們需要實現setValue:forUndefinedKey:這個方法能過濾掉不存在的鍵值 -(void)setValue:(id)value forUndefinedKey:(NSString *)key{ //這里我們不需要寫任何內容 } person.key1 = dictionary[@"key1"]; person.key2 = dictionary[@"key2"]; person.key3 = dictionary[@"key3"]; [person setValuesForKeysWithDictionary:dictionary]; NSLog(@"\n%@\n%@\n%@\n", person.key1,person.key2,person.key3); //輸出結果 test1 test2 test3 //2.3如果dictionar中的key與model中的變量名字不同,怎么賦值? //還是從setValue:forUndefinedKey:這個方法入手 -(void)setValue:(id)value forUndefinedKey:(NSString *)key{ if ([key isEqualToString:@"key2"]) { self.id = value; } person.key1 = dictionary[@"key1"]; person.id = dictionary[@"key2"]; person.key3 = dictionary[@"key3"]; [person setValuesForKeysWithDictionary:dictionary]; NSLog(@"\n%@\n%@\n%@\n", person.key1,person.id,person.key3); //輸出結果 test1 test2 test3
KVC集合屬性操作
KVC
提供的valueForKeyPath:
方法非常強大,可以通過該方法對集合對象進行“深入”操作,在其keyPath
中嵌套集合運算符,例如求一個數組中對象某個屬性的count
。(集合對象主要指NSArray
和NSSet
,但不包括NSDictionary
)
上面表達式主要分為三部分,left
部分是要操作的集合對象,如果調用KVC
的對象本來就是集合對象,則left
可以為空。中間部分是表達式,表達式一般以@符號開頭。后面是進行運算的屬性。
- 為了驗證操作符,我們需要先建立一個Model類
@interface Transaction : NSObject
@property (nonatomic, strong) NSString *payee;
@property (nonatomic, strong) NSNumber *amount;
@property (nonatomic, strong) NSDate *date;
@end
@interface BankAccount : NSObject
@property (nonatomic, strong) NSArray *transcationArray;
@end
集合操作符
處理集合包含的對象,并根據操作符的不同返回不同的類型,返回值以
NSNumber
為主。
//@avg用來計算集合中right keyPath指定的屬性的平均值
NSNumber *transactionAverage = [bankAccount.transcationArray valueForKeyPath:@"@avg.amount"];
NSLog(@"@avg = %@", transactionAverage);
//@count用來計算集合的總數
NSNumber *numberOfTransactions = [bankAccount.transcationArray valueForKeyPath:@"@count"];
NSLog(@"@count = %@", numberOfTransactions);
//備注:@count操作符比較特殊,它不需要寫right keyPath,即使寫了也會被忽略。
//@sum用來計算集合中right keyPath指定的屬性的總和。
NSNumber *amountSum = [bankAccount.transcationArray valueForKeyPath:@"@sum.amount"];
NSLog(@"@sum = %@", amountSum);
//@max用來查找集合中right keyPath指定的屬性的最大值
NSNumber *amountMax = [bankAccount.transcationArray valueForKeyPath:@"@max.amount"];
NSLog(@"@max = %@", amountMax);
//@min用來查找集合中right keyPath指定的屬性的最小值。
NSNumber *amountMin = [bankAccount.transcationArray valueForKeyPath:@"@min.amount"];
NSLog(@"@min = %@", amountMin);
數組操作符
根據操作符的條件,將符合條件的對象包含在數組中返回。
//@unionOfObjects將集合對象中,所有payee對象放在一個數組中并返回
NSArray *payees = [bankAccount.transcationArray valueForKeyPath:@"@unionOfObjects.payee"];
NSLog(@"@unionOfObjects = %@", payees);
//@distinctUnionOfObjects將集合對象中,所有payee對象放在一個數組中,并將數組進行去重后返回。
NSArray *distinctPayees = [bankAccount.transcationArray valueForKeyPath:@"@distinctUnionOfObjects.payee"];
NSLog(@"@distinctUnionOfObjects = %@", distinctPayees);
//注意:以上兩個方法中,如果操作的屬性為nil,在添加到數組中時會導致Crash。
嵌套操作符
處理集合對象中嵌套其他集合對象的情況,返回結果也是一個集合對象。
//@distinctUnionOfArrays是用來操作集合內部的集合對象,將所有right keyPath對應的對象放在一個數組中,并進行排重。
NSArray *collectedPayees = [allArray valueForKeyPath:@"@unionOfArrays.payee"];
NSLog(@"@unionOfArrays = %@", collectedPayees);
//@distinctUnionOfSets是用來操作集合內部的集合對象,將所有right keyPath對應的對象放在一個set中,并進行排重。
NSArray *collectedDistinctPayees = [allArray valueForKeyPath:@"@distinctUnionOfArrays.payee"];
NSLog(@"@distinctUnionOfArrays = %@", collectedDistinctPayees);
KVC與容器類
對象的屬性可以是一對一的,也可以是一對多的。一對多的屬性要么是有序的(數組),要么是無序的(集合)。
??:根據
KVO
的實現原理,是在運行時生成新的子類并重寫其setter
方法,在其內容發生改變時發送消息。但這只是對屬性直接進行賦值會觸發,如果屬性是容器對象,對容器對象進行add
或remove
操作,則不會調用KVO
的方法。可以通過KVC
對應的API
來配合使用,使容器對象內部發生改變時也能觸發KVO
。在進行容器對象操作時,先通過
key
或者keyPath
獲取集合對象,然后再對容器對象進行add
或remove
等操作時,就會觸發KVO
的消息通知了。
KVC與有序容器(NSMutableArray)
取值方法
- 通過key
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//該方法返回一個可變有序數組
- 通過keyPath
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
//該方法返回一個可變有序數組
NSMutableArray取值底層實現
當調用
mutableArrayValueForKey
的代碼時,其搜索方式如下:你需要先看一下這張流程圖,大致知道如何運轉的,之后再看文字描述,仔細了解其機制
-
搜索
insertObject:in<Key>AtIndex:
,removeObjectFrom<Key>AtIndex:
或者insert<Key>AdIndexes
,remove<Key>AtIndexes
格式的方法
如果至少找到一個insert
方法和一個remove
方法,那么同樣返回一個可以響應NSMutableArray
所有方法代理集合(類名是NSKeyValueFastMutableArray
),那么給這個代理集合發送NSMutableArray
的方法,以insertObject:in<Key>AtIndex:
,removeObjectFrom<Key>AtIndex:
或者insert<Key>AdIndexes
,remove<Key>AtIndexes
組合的形式調用。當對象接收一個
mutableArrayValueForKey:
消息并實現可選替換方法,例如replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
方法,代理對象會在適當的情況下使用它們,以獲得最佳性能。 -
如果上步的方法沒有找到,則搜索
set<Key>:
格式的方法,如果找到,那么發送給代理集合的NSMutableArray
最終都會調用set<Key>:
方法。也就是說,
mutableArrayValueForKey:
取出的代理集合修改后,用set<Key>:
重新賦值回去去。這樣做效率會低很多。所以推薦實現上面的方法。 如果上一步的方法還還沒有找到,再檢查類方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES(默認行為),會按_<key>
,<key>
,的順序搜索成員變量名,如果找到,那么發送的NSMutableArray
消息方法直接交給這個成員變量處理。如果還是找不到,則調用
valueForUndefinedKey:
KVC與無序容器(NSMutableSet)
取值方法
- 通過key
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
//方法返回一個可變的無序數組
- 通過keyPath
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
//方法返回一個可變的無序數組
NSMutableSet取值底層實現
當調用
NSMutableSet
的代碼時,其搜索方式如下:你需要先看一下這張流程圖,大致知道如何運轉的,之后再看文字描述,仔細了解其機制
- 搜索
addObject<Key>Object:
,remove<Key>Object:
或者add<Key>
,remove<Key>
格式的方法
如果至少找到一個insert
方法和一個remove
方法,那么同樣返回一個可以響應NSMutableSet
所有方法代理集合(類名是NSKeyValueFastMutableSet2
),那么給這個代理集合發送NSMutableSet
的方法,以addObject<Key>Object:
,remove<Key>Object:
或者add<Key>
,remove<Key>
組合的形式調用。還有兩個可選實現的接口:intersect<Key> , set<Key>:
。 - 如果
receiver
是ManagedObject
,那么就不會繼續搜索。 - 如果上一步的方法沒有找到,則搜索
set<Key>
: 格式的方法,如果找到,那么發送給代理集合的NSMutableSet
最終都會調用set<Key>:
方法。 也就是說,mutableSetValueForKey
取出的代理集合修改后,用set<Key>:
重新賦值回去去。這樣做效率會低很多。所以推薦實現上面的方法。 - 如果上一步的方法還沒有找到,再檢查類方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES
(默認行為),會按_<key>
,<key>
的順序搜索成員變量名,如果找到,那么發送的NSMutableSet
消息方法直接交給這個成員變量處理。 - 如果還是找不到,調用
valueForUndefinedKey:
可見,除了檢查receiver
是ManagedObject
以外,其搜索順序和mutableArrayValueForKey
基本一至
KVC異常處理
- key或者keyPath發生錯誤
當根據
KVC
搜索規則,沒有搜索到對應的key
或者keyPath
,則會調用對應的異常方法。異常方法的默認實現,在異常發生時會拋出一個NSUndefinedKeyException
的異常,并且應用程序Crash
我們可以重寫下面兩個方法:
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- 傳參為nil
通常情況下,KVC不允許你要在調用
setValue:屬性值 forKey:
(或者keyPath)時對非對象傳遞一個nil
的值。因為值類型是不能為nil
的。如果你不小心傳了,KVC會調用setNilValueForKey:
方法。這個方法默認是拋出異常,所以一般而言最好還是重寫這個方法。
我們可以重寫這個方法:
-(void)setNilValueForKey:(NSString *)key{
NSLog(@"不能將%@設成nil",key);
}
KVC處理非對象
KVC是支持基礎數據類型和結構體的,可以在
setter
和getter
的時候,通過NSValue
和NSNumber
來轉換為OC
對象。該方法valueForKey:
總是返回一個id對象,如果原本的變量類型是值類型或者結構體,返回值會封裝成NSNumber
或者NSValue
對象。這兩個類會處理從數字,布爾值到指針和結構體任何類型。然后開發者需要手動轉換成原來的類型。盡管valueForKey:
會自動將值類型封裝成對象,但是setValue:forKey:
卻不行。你必須手動將值類型轉換成NSNumber
或者NSValue
類型,才能傳遞過去。
- 可以調用
initWithBool:
方法對基礎數據類型進行包裝
@property (nonatomic, assign, readonly) BOOL boolValue;
- (NSNumber *)initWithBool:(BOOL)value
KVC屬性驗證
KVC提供了屬性值,用來驗證key對應的Value是否可用的方法
-
在調用
KVC
時可以先進行驗證,驗證通過下面兩個方法進行,支持key
和keyPath
兩種方式。驗證方法默認實現返回YES
,可以通過重寫對應的方法修改驗證邏輯。驗證方法需要我們手動調用,并不會在進行
KVC
的過程中自動調用。
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
這個方法的默認實現是去探索類里面是否有一個這樣的方法:
-(BOOL)validate<Key>:error:
如果有這個方法,就調用這個方法來返回,沒有的話就直接返回YES
@implementation Address
-(BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{ //在implementation里面加這個方法,它會驗證是否設了非法的value
NSString* country = *value;
country = country.capitalizedString;
if ([country isEqualToString:@"Japan"]) {
return NO; //如果國家是日本,就返回NO,這里省略了錯誤提示,
}
return YES;
}
@end
NSError* error;
id value = @"japan";
NSString* key = @"country";
BOOL result = [add validateValue:&value forKey:key error:&error]; //如果沒有重寫-(BOOL)-validate<Key>:error:,默認返回Yes
if (result) {
NSLog(@"鍵值匹配");
[add setValue:value forKey:key];
}
else{
NSLog(@"鍵值不匹配"); //不能設為日本,其他國家都行
}
NSString* country = [add valueForKey:@"country"];
NSLog(@"country:%@",country);
//打印結果
KVCDemo[867:58871] 鍵值不匹配
KVCDemo[867:58871] country:China
KVC適用場景
動態的取值和設值
利用KVC動態的取值和設值是最基本的用途了。相信每一個iOS開發者都能熟練掌握
Model和字典轉換
在上面KVC批量操作已闡述
用KVC來訪問和修改私有變量
根據上面的實現原理我們知道,
KVC
本質上是操作方法列表以及在內存中查找實例變量。我們可以利用這個特性訪問類的私有變量,例如下面在.m
中定義的私有成員變量和屬性,都可以通過KVC
的方式訪問。這個操作對
readonly
的屬性,@protected
的成員變量,都可以正常訪問。如果不想讓外界訪問類的成員變量,則可以將accessInstanceVariablesDirectly
屬性賦值為NO
。
修改一些控件的內部屬性
這也是iOS開發中必不可少的小技巧。眾所周知很多UI控件都由很多內部UI控件組合而成的,但是Apple度沒有提供這訪問這些控件的API,這樣我們就無法正常地訪問和修改這些控件的樣式。而KVC在大多數情況可下可以解決這個問題。