1、KVC簡介
KVC全稱是Key Value Coding
,定義在NSKeyValueCoding.h
文件中,翻譯成中文是鍵值碼
,是由NSKeyValueCoding
非正式協議啟用的一種機制,對象采用這種機制來提供對其屬性的間接訪問,這種間接訪問機制補充了實例變量及其關聯的訪問器方法提供的直接訪問。KVC的定義是通過NSObject的拓展類來實現的,Objective-C中有個顯式的NSKeyValueCoding類別名,所以可以說在日常開發中凡是直接或間接繼承或自NSObject的對象都可以使用KVC機制。
2、KVC的基礎使用
2.1、KVC處理對象屬性
KVC通過 valueForKey:
和 setValue:forKey:
來間接的獲取和設置對象的屬性值。
關于這兩個方法的定義如下:
valueForKey:
- Returns the value of a property named by the key parameter. If the property named by the key cannot be found according to the rules described in Accessor Search Patterns, then the object sends itself a valueForUndefinedKey: message. The default implementation of valueForUndefinedKey: raises an NSUndefinedKeyException, but subclasses may override this behavior and handle the situation more gracefully.
【譯】valueForKey:
-返回由key參數命名的屬性的值。如果根據訪問者搜索模式中描述的規則找不到由key
命名的屬性,則該對象向自身發送一條valueForUndefinedKey:
消息。valueForUndefinedKey:
默認實現拋出一個NSUndefinedKeyException
異常,但是子類可以覆蓋此行為,并更優雅地處理該情況。
setValue:forKey:
- Sets the value of the specified key relative to the object receiving the message to the given value. The default implementation of setValue:forKey: automatically unwraps NSNumber and NSValue objects that represent scalars and structs and assigns them to the property. See Representing Non-Object Values for details on the wrapping and unwrapping semantics.
If the specified key corresponds to a property that the object receiving the setter call does not have, the object sends itself a setValue:forUndefinedKey: message. The default implementation of setValue:forUndefinedKey: raises an NSUndefinedKeyException. However, subclasses may override this method to handle the request in a custom manner.
【譯】setValue:forKey:
-將接收消息的對象指定的key
設置為給定值。setValue:forKey:
默認實現會自動把表示標量和結構體的NSNumber
和NSValue
對象解包,并賦值給屬性。如果指定key
所對應的屬性沒有對應的setter
實現,則該對象會向自身發送setValue:forUndefinedKey:
消息,而該消息的默認實現會拋出一個NSUndefinedKeyException
的異常。但是子類可以重寫此方法以自定義方式處理請求
看下面的例子:
@interface Person : NSObject{
// NSString *name;
// NSString *_name;
// NSString *_isName;
// NSString *isName;
}
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
@end
int main(int argc, const char *argv[])
{
@autoreleasepool {
Person *person = [[Person alloc]init];
[person setValue:@"劉德華" forKey:@"name"];
NSLog(@"name:%@",[person valueForKey:@"name"]);
}
return 0;
}
打印結果: 2020-03-05 17:00:44.647980+0800 KVCDemo[47069:2104780] name:劉德華
你一定注意到了這段代碼中Person類中的注釋部分的代碼,經過小編驗證不論是以何種形式,程序輸出的結果都是正確的。這里涉及到的其實KVC的設置和取值規則,會在下面的章節中講解到KVC的設置和取值原理。
2.2、KVC使用KeyPath
在開發過程中,一個類的成員變量有可能是自定義類或其他的復雜數據類型,你可以先用KVC獲取該屬性,然后再次用KVC來獲取這個自定義類的屬性, 但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑keyPath。顧名思義,就是按照路徑尋找key。
valueForKeyPath:
- Returns the value for the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key—that is, for which the default implementation ofvalueForKey:
cannot find an accessor method—receives avalueForUndefinedKey:
message.
【譯】valueForKeyPath:
-返回相對于接收者的指定key path
的值。key path
路徑序列中不符合特定鍵的鍵值編碼的任何對象(即,默認實現valueForKey:
無法找到訪問器方法)均會接收到valueForUndefinedKey:
消息。
setValue:forKeyPath:
- Sets the given value at the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key receives asetValue:forUndefinedKey:
message.
【譯】setValue:forKeyPath:
-將該消息接收者的指定key path
的值設置為給定值。key path
路徑序列中不符合特定鍵的鍵值編碼的任何對象都將收到setValue:forUndefinedKey:
消息。
看下面例子:
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Student
@end
@interface Person : NSObject{
Student *_student;
}
@end
@implementation Person
@end
int main(int argc, const char *argv[])
{
@autoreleasepool {
//方式一
Person *person = [[Person alloc]init];
Student *student = [[Student alloc]init];
[person setValue:student forKey:@"student"];
[student setValue:@"小明" forKey:@"name"];
NSString *name = [[person valueForKey:@"student"] valueForKey:@"name"];
NSLog(@"name:%@",name);
//方式二
[person setValue:@"小小" forKeyPath:@"student.name"];
NSLog(@"name:%@",[person valueForKeyPath:@"student.name"]);
}
return 0;
}
打印結果:
2020-03-05 17:28:35.709176+0800 KVCDemo[47571:2124832] name:小明
2020-03-05 17:28:35.709646+0800 KVCDemo[47571:2124832] name:小小
從打印結果來看我們成功的通過keyPath設置了student的值。 KVC對于keyPath
搜索機制第一步就是分離key,用小數點.來分割key,然后再像普通key一樣按照先前介紹的順序搜索下去。
2.3、KVC處理數值和結構體類型屬性
在前面小節中的內容都是對對象類型的變量進行的設值取值,那么如果變量類型是數值類型或者是結構,那么KVC是否也是可以進行設置和取值的呢?答案是肯定的。如果原本的變量類型是值類型或者結構體,返回值會封裝成NSNumber
或者NSValue
對象。這兩個類會處理從數字,布爾值到指針和結構體任何類型。然后開發者需要手動轉換成原來的類型。盡管valueForKey:
會自動將值類型封裝成對象,但是 setValue:forKey:
卻不行。你必須手動將值類型轉換成NSNumber或者NSValue類型,才能傳遞過去。因為傳遞進去和取出來的都是id類型,所以需要開發者自己擔保類型的正確性,運行時Objective-C在發送消息的會檢查類型,如果錯誤會直接拋出異常。
看下面例子:
typedef struct {
float x, y, z;
} ThreeFloats;
@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic) ThreeFloats threeFloats;
@end
@implementation Person
@end
int main(int argc, const char *argv[])
{
@autoreleasepool {
//處理數值類型
Person *person = [[Person alloc]init];
[person setValue:[NSNumber numberWithInteger:20] forKey:@"age"];
NSLog(@"age:%@", [person valueForKey:@"age"]);
//處理結構體
ThreeFloats floats = { 1., 2., 3. };
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslut = [person valueForKey:@"threeFloats"];
NSLog(@"%@", reslut);
ThreeFloats th;
[reslut getValue:&th];
NSLog(@"%f - %f - %f", th.x, th.y, th.z);
}
return 0;
}
打印結果:
2020-03-05 17:49:12.490826+0800 KVCDemo[47898:2139908] age:20
2020-03-05 17:49:12.491413+0800 KVCDemo[47898:2139908] {length = 12, bytes = 0x0000803f0000004000004040}
2020-03-05 17:49:12.491461+0800 KVCDemo[47898:2139908] 1.000000 - 2.000000 - 3.000000
需要注意的是我們不能直接將一個數值通過KVC賦值的,我們需要把數據轉為NSNumber和NSValue類型傳入,那到底哪些類型數據要用NSNumber封裝哪些類型數據要用NSValue封裝呢?看下面這些方法的參數類型就知道了:
可以使用NSNumber的數據類型有:
+ (NSNumber)numberWithChar:(char)value;
+ (NSNumber)numberWithUnsignedChar:(unsignedchar)value;
+ (NSNumber)numberWithShort:(short)value;
+ (NSNumber)numberWithUnsignedShort:(unsignedshort)value;
+ (NSNumber)numberWithInt:(int)value;
+ (NSNumber)numberWithUnsignedInt:(unsignedint)value;
+ (NSNumber)numberWithLong:(long)value;
+ (NSNumber)numberWithUnsignedLong:(unsignedlong)value;
+ (NSNumber)numberWithLongLong:(longlong)value;
+ (NSNumber)numberWithUnsignedLongLong:(unsignedlonglong)value;
+ (NSNumber)numberWithFloat:(float)value;
+ (NSNumber)numberWithDouble:(double)value;
+ (NSNumber)numberWithBool:(BOOL)value;
+ (NSNumber)numberWithInteger:(NSInteger)valueNS_AVAILABLE(10_5,2_0);
+ (NSNumber*)numberWithUnsignedInteger:(NSUInteger)valueNS_AVAILABLE(10_5,2_0);
可以使用NSValue的數據類型有:
+ (NSValue)valueWithCGPoint:(CGPoint)point;
+ (NSValue)valueWithCGSize:(CGSize)size;
+ (NSValue)valueWithCGRect:(CGRect)rect;
+ (NSValue)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);
2.4、KVC處理集合
KVC提供了對集合類型處理的方法。
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
這些返回行為像NSMutableArray
對象的代理對象。mutableSetValueForKey:
和mutableSetValueForKeyPath:
這些返回行為像NSMutableSet
對象的代理對象。mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
這些返回行為像NSMutableOrderedSet
對象的代理對象。
看下面的例子:
@interface Person : NSObject
@property (nonatomic, copy) NSArray *classArr;
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
@end
int main(int argc, const char *argv[])
{
@autoreleasepool {
Person *person = [[Person alloc]init];
person.classArr = @[@"Chinese",@"Mathematics",@"English"];
[person setValue:@"小明" forKey:@"name"];
[person mutableArrayValueForKey:@"classArr"];
NSLog(@"name:%@,class:%@",[person valueForKey:@"name"],[person mutableArrayValueForKey:@"classArr"]);
}
return 0;
}
打印結果:
2020-03-05 22:01:58.540062+0800 KVCDemo[52012:2424108] name:小明,class:(
Chinese, Mathematics,English)
這里只演示了mutableArrayValueForKey
的使用,其他的方法使用類同,在這里就不多贅述了。
2.5、KVC處理集合運算符
KVC同時還提供了集合運算符,利用這些集合運算符可以針對集合做一些高效的統計運算。這些集合運算符主要分為三大類,如下所示:
聚合操作符
- @avg: 返回集合中指定對象屬性的平均值
- @count: 返回集合中指定對象屬性的個數
- @max: 返回集合中指定對象屬性的最大值
- @min: 返回集合中指定對象屬性的最小值
- @sum: 返回集合中指定對象屬性值之和
數組操作符
- @distinctUnionOfObjects: 返回集合中指定對象屬性的集合,且會進行去重操作
- @unionOfObjects: 返回集合中指定對象屬性的集合,并不會刪除相同元素。
嵌套操作符
- @distinctUnionOfArrays: 返回指定的屬性相對應的所有集合的組合的不同對象集合,并會刪除相同的元素
- @unionOfArrays: 返回指定的屬性相對應的所有集合的組合的不同對象集合,但是不會刪除相同元素
- @distinctUnionOfSets: 返回指定的屬性相對應的所有集合的組合中的不同對象集合,并刪除相同元素,返回的是 NSSet
看下面的例子:
@interface Book : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGFloat price;
@end
@implementation Book
@end
int main(int argc, const char *argv[])
{
@autoreleasepool {
Book *book1 = [Book new];
book1.name = @"編程珠璣";
book1.price = 50;
Book *book2 = [Book new];
book2.name = @"Java編程思想";
book2.price = 20;
Book *book3 = [Book new];
book3.name = @"漫畫算法";
book3.price = 30;
Book *book4 = [Book new];
book4.name = @"算法圖解";
book4.price = 30;
NSArray *arrBooks = @[book1, book2, book3, book4];
//計算總價
NSNumber *sum = [arrBooks valueForKeyPath:@"@sum.price"];
NSLog(@"sum:%f", sum.floatValue);
//計算平均價格
NSNumber *avg = [arrBooks valueForKeyPath:@"@avg.price"];
NSLog(@"avg:%f", avg.floatValue);
//計算書本數量
NSNumber *count = [arrBooks valueForKeyPath:@"@count"];
NSLog(@"count:%f", count.floatValue);
//計算最小的書本價格
NSNumber *min = [arrBooks valueForKeyPath:@"@min.price"];
NSLog(@"min:%f", min.floatValue);
//計算最大的書本價格
NSNumber *max = [arrBooks valueForKeyPath:@"@max.price"];
NSLog(@"max:%f", max.floatValue);
//返回書本的價格集合
NSArray *distinctPrice = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
NSLog(@"distinctPrice:%@", distinctPrice);
//返回書本的價格集合
NSArray *unionPrice = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
NSLog(@"unionPrice:%@", unionPrice);
NSArray *arr1 = @[book1,book2];
NSArray *arr2 = @[book3,book4];
NSArray *arr = @[arr1,arr2];
NSArray *collectedDistinctPrice = [arr valueForKeyPath:@"@distinctUnionOfArrays.price"];
NSLog(@"collectedDistinctPrice:%@", collectedDistinctPrice);
NSArray *collectedPrice = [arr valueForKeyPath:@"@unionOfArrays.price"];
NSLog(@"collectedPrice:%@", collectedPrice);
}
return 0;
}
打印結果:
2020-03-05 22:55:37.927846+0800 KVCDemo[52895:2466440] sum:130.000000
2020-03-05 22:55:37.928381+0800 KVCDemo[52895:2466440] avg:32.500000
2020-03-05 22:55:37.928467+0800 KVCDemo[52895:2466440] count:4.000000
2020-03-05 22:55:37.928526+0800 KVCDemo[52895:2466440] min:20.000000
2020-03-05 22:55:37.928568+0800 KVCDemo[52895:2466440] max:50.000000
2020-03-05 22:55:37.928693+0800 KVCDemo[52895:2466440] distinctPrice:(
20,30,50)
2020-03-05 22:55:37.928783+0800 KVCDemo[52895:2466440] unionPrice:(
50,20,30,30)
2020-03-05 22:55:37.928865+0800 KVCDemo[52895:2466440] collectedDistinctPrice:(
20,30, 50)
2020-03-05 22:55:37.928931+0800 KVCDemo[52895:2466440] collectedPrice:(
50, 20,30,30)
2.5、KVC處理字典
當對NSDictionary對象使用KVC時,valueForKey
的表現行為和objectForKey:
一樣。所以使用valueForKeyPath:
用來訪問多層嵌套的字典是比較方便的。
dictionaryWithValuesForKeys:
- Returns the values for an array of keys relative to the receiver. The method calls valueForKey: for each key in the array. The returned NSDictionary contains values for all the keys in the array.
【譯】返回相對于接收者的 key 數組的值。該方法會為數組中的每個 key 調用valueForKey:。 返回的 NSDictionary 包含數組中所有鍵的值。setValuesForKeysWithDictionary:
- Sets the properties of the receiver with the values in the specified dictionary, using the dictionary keys to identify the properties. The default implementation invokes setValue:forKey: for each key-value pair, substituting nil for NSNull objects as required.
【譯】使用字典鍵標識屬性,然后使用字典中的對應值來設置該消息接收者的屬性值。默認實現會對每一個鍵值對調用 setValue:forKey:。設置時需要將 nil 替換成 NSNull。
看下面的例子:
@interface Address : NSObject
@property (nonatomic, copy) NSString *country;
@property (nonatomic, copy) NSString *province;
@property (nonatomic, copy) NSString *city;
@property (nonatomic, copy) NSString *district;
@end
@implementation Address
@end
int main(int argc, const char *argv[])
{
@autoreleasepool {
//模型轉字典
Address *address = [Address new];
address.country = @"China";
address.province = @"Guang Dong";
address.city = @"Shen Zhen";
address.district = @"Nan Shan";
NSArray *arr = @[@"country", @"province", @"city", @"district"];
NSDictionary *dict = [address dictionaryWithValuesForKeys:arr];
NSLog(@"%@", dict);
//字典轉模型
NSDictionary *modifyDict = @{ @"country": @"China", @"province": @"Guang Dong", @"city": @" Shen Zhen", @"district": @"Nan Shan" };
[address setValuesForKeysWithDictionary:modifyDict]; //用key Value來修改Model的屬性
NSLog(@"country:%@ province:%@ city:%@ district:%@", address.country, address.province, address.city, address.district);
}
return 0;
}
打印結果:
2020-03-05 23:07:20.645255+0800 KVCDemo[53037:2474708] {
city = "Shen Zhen";
country = China;
district = "Nan Shan";
province = "Guang Dong";
}
2020-03-05 23:07:20.646032+0800 KVCDemo[53037:2474708] country:China province:Guang Dong city: Shen Zhen district:Nan Shan
2.6、KVC處理異常
在使用KVC開發的過程中難免會出現一些失誤,諸如寫錯了key或者在設置的時候傳遞了nil的值,KVC中專門提供了處理這些異常的方法。
2.6.1、KVC處理nil異常
通常情況下,KVC不允許你要在調用setValue:forKey:
或者setValue:forKeyPath:
時對非對象傳遞一個nil的值。很簡單,因為值類型是不能為nil的。如果你不小心傳了,KVC會調用setNilValueForKey:
方法。這個方法默認是拋出異常,所以一般而言最好還是重寫這個方法。
看下面例子:
@interface Person : NSObject
{
int age;
}
@end
@implementation Person
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"不能將%@設成nil", key);
}
@end
int main(int argc, const char *argv[])
{
@autoreleasepool {
Person *person = [[Person alloc] init];
[person setValue:nil forKey:@"age"];
NSLog(@"age:%@", [person valueForKey:@"age"]);
}
return 0;
}
打印結果:
2020-03-05 23:17:24.713116+0800 KVCDemo[53187:2481241] 不能將age設成nil
2020-03-05 23:17:24.713661+0800 KVCDemo[53187:2481241] age:0
2.6.2、處理UndefinedKey異常
通常情況下,KVC不允許你要在調用setValue:forKey:
或者setValue:forKeyPath:
時對不存在的key進行操作。 否則會報錯發生崩潰,重寫setValue: forUndefinedKey:
和valueForUndefinedKey:
方法避免崩潰。
看下面的例子:
@interface Person : NSObject
@end
@implementation Person
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出現異常,該key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出現異常,該key不存在%@", key);
}
@end
int main(int argc, const char *argv[])
{
@autoreleasepool {
Person *person = [[Person alloc] init];
[person setValue:nil forKey:@"age"];
NSLog(@"age:%@", [person valueForKey:@"age"]);
}
return 0;
}
2.7、KVC鍵值驗證(Key-Value Validation)
KVC提供了驗證Key對應的Value是否可用的方法,調用validateValue:forKey:error:
(或validateValue:forKeyPath:error:
)方法時,協議的默認實現會在接收驗證消息的對象(或keyPath的對象)中根據key搜索是否有方法validate<Key>:error:
實現。如果對象沒有這種方法,則默認情況下驗證成功,并且默認實現返回YES
。當存在特定于屬性的驗證方法時,默認實現將返回調用該方法的結果。
由于特定于屬性的驗證方法通過引用接收值和錯誤參數,因此驗證具有三種可能的結果:
- 驗證成功,返回 YES,value不做修改。
- 驗證失敗,返回 NO,value不做修改,如果調用者提供了 NSError 的話,就把錯誤引用設置為指示錯誤原因的NSError對象。
- 驗證失敗,返回 YES,但是創建了一個新的有效的屬性值作為替代。在返回之前,該方法將值引用修改為指向新值對象。 進行修改時,即使值對象是可變的,該方法也總是創建一個新對象,而不是修改舊對象。
看下面的例子:
@interface Person : NSObject
{
int age;
}
@end
@implementation Person
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *_Nullable __autoreleasing *)outError {
NSNumber *age = *ioValue;
if (age.integerValue == 10) {
return NO;
}
return YES;
}
@end
int main(int argc, const char *argv[])
{
@autoreleasepool {
Person *person = [[Person alloc] init];
NSNumber *age = @10;
NSError *error;
NSString *key = @"age";
BOOL isValid = [person validateValue:&age forKey:key error:&error];
if (isValid) {
NSLog(@"鍵值匹配");
[person setValue:age forKey:key];
} else {
NSLog(@"鍵值不匹配");
}
NSLog(@"age:%@", [person valueForKey:@"age"]);
}
return 0;
}
打印結果:
2020-03-05 23:42:22.811163+0800 KVCDemo[53621:2503057] 鍵值不匹配
2020-03-05 23:42:22.811752+0800 KVCDemo[53621:2503057] age:0
3、KVC設值和取值原理
在前面的章節中的我們探索了KVC的基本使用,但是還是不知道KVC的設值和取值規則,只有把這些規則都弄清楚了,才能在實際開發中得心應手。
KVC的設值和取值規則針對于對象類型、可變數組、可變有序集、可變集的規則有所不同。
3.1、基礎Getter搜索模式
這是valueForKey:
的默認實現,給定一個key
當做輸入參數,開始下面的步驟,在這個接收valueForKey:
方法調用的類內部進行操作。
- 通過
getter
方法搜索實例,例如get<Key>
,<key>
,is<Key>
,_<key>
的拼接方案。按照這個順序,如果發現符合的方法,就調用對應的方法并拿著結果跳轉到第五步。否則,就繼續到下一步。 - 如果沒有找到簡單的
getter
方法,則搜索其匹配模式的方法countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
。如果找到其中的第一個和其他兩個中的一個,則創建一個集合代理對象,該對象響應所有NSArray
的方法并返回該對象。否則,繼續到第三步。代理對象隨后將NSArray
接收到的countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
的消息給符合KVC規則的調用方。當代理對象和KVC調用方通過上面方法一起工作時,就會允許其行為類似于NSArray
一樣。 - 如果沒有找到
NSArray
簡單存取方法,或者NSArray
存取方法組。則查找有沒有countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
命名的方法。如果找到三個方法,則創建一個集合代理對象,該對象響應所有NSSet
方法并返回。否則,繼續執行第四步。此代理對象隨后轉換countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
方法調用到創建它的對象上。實際上,這個代理對象和NSSet
一起工作,使得其表象上看起來是NSSet
。 - 如果沒有發現簡單
getter
方法,或集合存取方法組,以及接收類方法accessInstanceVariablesDirectly
是返回YES的。搜索一個名為_<key>
、_is<Key>
、<key>
、is<Key>
的實例,根據他們的順序。如果發現對應的實例,則立刻獲得實例可用的值并跳轉到第五步,否則,跳轉到第六步。 - 如果取回的是一個對象指針,則直接返回這個結果。如果取回的是一個基礎數據類型,但是這個基礎數據類型是被
NSNumber
支持的,則存儲為NSNumber
并返回。如果取回的是一個不支持NSNumber
的基礎數據類型,則通過NSValue進行存儲并返回。 - 如果所有情況都失敗,則調用
valueForUndefinedKey:
方法并拋出異常,這是默認行為。但是子類可以重寫此方法。
3.2、基礎Setter搜索模式
這是setValue:forKey:
的默認實現,給定輸入參數value
和key
。試圖在接收調用對象的內部,設置屬性名為key
的value
,通過下面的步驟:
- 查找
set<Key>:
或_set<Key>
命名的setter
,按照這個順序,如果找到的話,調用這個方法并將值傳進去(根據需要進行對象轉換)。 - 如果沒有發現一個簡單的
setter
,但是accessInstanceVariablesDirectly
類屬性返回YES,則查找一個命名規則為_<key>
、_is<Key>
、<key>
、is<Key>
的實例變量。根據這個順序,如果發現則將value
賦值給實例變量。 - 如果沒有發現
setter
或實例變量,則調用setValue:forUndefinedKey:
方法,并默認提出一個異常,但是一個NSObject的子類可以提出合適的行為。
3.3、NSMutableArray搜索模式
這是mutableArrayValueForKey:
的默認實現,給一個key
當做輸入參數。在接收訪問器調用的對象中,返回一個名為key
的可變代理數組,這個代理數組就是用來響應外界KVO的對象,通過下面的步驟進行查找:
- 查找一對方法
insertObject:in<Key>AtIndex:
和removeObjectFrom<Key>AtIndex:
(相當于NSMutableArray
的原始方法insertObject:atIndex:
和removeObjectAtIndex:
)或者方法名是insert<Key>:atIndexes:
和remove<Key>AtIndexes:
(相當于NSMutableArray
的原始方法insertObjects:atIndexes:
和removeObjectsAtIndexes:
)。如果找到最少一個insert
方法和最少一個remove
方法,則返回一個代理對象,來響應發送給NSMutableArray
的組合消息insertObject:in<Key>AtIndex:
、removeObjectFrom<Key>AtIndex:
、insert<Key>:atIndexes:
,和remove<Key>AtIndexes:
消息。當對象接收一個mutableArrayValueForKey:
消息并實現可選替換方法,例如replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
方法,代理對象會在適當的情況下使用它們,以獲得最佳性能。 - 如果對象沒有可變數組方法,查找一個替代方法,命名格式為
set<Key>:
。在這種情況下,向mutableArrayValueForKey:
的原始響應者發送一個set<Key>:
消息,來返回一個代理對象來響應NSMutableArray
事件。 - 如果沒有可變數組的方法,也沒有找到訪問器,但接受響應的類
accessInstanceVariablesDirectly
屬性返回YES,則查找一個名為_<key>
或<key>
的實例變量。按照這個順序,如果找到實例變量,則返回一個代理對象。改對象將接收所有NSMutableArray
發送過來的消息,通常是NSMutableArray
或其子類。 - 如果所有情況都失敗,則返回一個可變的集合代理對象。當它接收
NSMutableArray
消息時,發送一個setValue:forUndefinedKey:
消息給接收mutableArrayValueForKey:
消息的原始對象。這個setValue:forUndefinedKey:
的默認實現是提出一個NSUndefinedKeyException
異常,但是子類可以重寫這個實現。
3.3、其他
還有NSMutableSet
和NSMutableOrderedSet
兩種搜索模式,這兩種搜索模式和NSMutableArray
步驟相同,只是搜索和調用的方法不同。詳細的搜索方法都可以在KVC官方文檔中找到,再套用上面的流程即可理解。
4、自定義KVC
在上一個章節中我們分析了KVC的設值和取值規則,那么便可以遵照規則定義自己的KVC。自定義KVC主要是基于取值和設值兩個方面考慮。
4.1、自定義KVC設值
自定義KVC設值還是在setValue:forKey:
方法上面做文章,大致思路如下:
- 首先需要判斷傳進來的key是否為nil,如果為nil則直接返回,否則執行第2步;
- 找到相關方法
set<Key>
、_set<Key>
、setIs<Key>
是否有實現,如果有實現的話這直接調用這些方法,否則執行第3步; - 判斷
accessInstanceVariablesDirectly
方法的返回結果,如果返回NO
,拋出異常,否則執行第4步; - 按照
_<key>
、_is<Key>
、<key>
、is<Key>
順序查找成員變量,如果找到了則直接賦值,否則執行第5步; - 如果程序執行到這一步則說明按照搜索規則沒有找到相應的
key
,則直接拋出異常。
主要代碼如下:
- (void)ds_setValue:(nullable id)value forKey:(NSString *)key{
// 1:非空判斷一下
if (key == nil || key.length == 0) return;
// 2:找到相關方法 set<Key> _set<Key> setIs<Key>
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
if ([self ds_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"執行 %@ 方法",setKey);
return;
}else if ([self ds_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"執行 %@ 方法",_setKey);
return;
}else if ([self ds_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"執行 %@ 方法",setIsKey);
return;
}
// 3:判斷是否能夠直接賦值實例變量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.找相關實例變量進行賦值,按照這個順序查找_<key> _is<Key> <key> is<Key>
// 4.1 定義一個收集實例變量的可變數組
NSMutableArray *mArray = [self getIvarListName];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
// 4.2 獲取相應的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3 對相應的 ivar 設置值
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}
// 5:如果找不到相關實例
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
判斷方法是否實現:
- (BOOL)ds_performSelectorWithMethodName:(NSString *)methodName value:(id)value {
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
return YES;
}
return NO;
}
獲取實例變量數組的方法如下:
- (NSMutableArray *)getIvarListName {
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *ivarNameChar = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName == %@", ivarName);
[mArray addObject:ivarName];
}
free(ivars);
return mArray;
}
4.2、自定義KVC取值
自定義KVC設值還是在lg_valueForKey:
方法上面做文章,大致思路如下:
- 首先判斷key是否為nil,如果為nil,則直接返回,否則執行第2步;
- 按照順序找到相關方法
get<Key>
、<key>
、countOf<Key>
、objectIn<Key>AtIndex
是否實現,如果有其中一個實現則直接調用這個方法,否則執行第3步; - 判斷
accessInstanceVariablesDirectly
方法的返回結果,如果返回NO
,拋出異常,否則執行第4步; - 按照
_<key>
、_is<Key>
、<key>
、is<Key>
順序查找成員變量,如果找到了則直接取值,否則執行第5步; - 如果執行到這一步,則返回空。
主要代碼如下:
- (nullable id)ds_valueForKey:(NSString *)key {
// 1:刷選key 判斷非空
if (key == nil || key.length == 0) {
return nil;
}
// 2:找到相關方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@", Key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@", Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:", Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
} else if ([self respondsToSelector:NSSelectorFromString(key)]) {
return [self performSelector:NSSelectorFromString(key)];
} else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]) {
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i < num - 1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j < num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
// 3:判斷是否能夠直接賦值實例變量
if (![self.class accessInstanceVariablesDirectly]) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****", self] userInfo:nil];
}
// 4.找相關實例變量進行賦值 按照順序查找是否實現_<key> _is<Key> <key> is<Key>
// 4.1 定義一個收集實例變量的可變數組
NSMutableArray *mArray = [self getIvarListName];
NSString *_key = [NSString stringWithFormat:@"_%@", key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@", Key];
NSString *isKey = [NSString stringWithFormat:@"is%@", Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);
} else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);
} else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);
} else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);
}
return @"";
}
在這里的自定義是比較簡潔的一種寫法,并不夠完善,如果有興趣的可以閱讀DIS_KVC_KVO的源碼,對KVC和KVO都有比較全面詳細的自定義。