目錄
前言
KVC定義及API
KVC的使用
基本類型
集合類型
訪問非對象類型——結構體
集合操作符
層層嵌套
KVC底層原理
設值過程
取值過程
自定義KVC
setter方法
getter方法
KVC異常小技巧
自動轉換類型
設置空值
未定義的key
前言
在平時的開發中我們經常用到KVC賦值取值、字典轉模型,這篇文章我們來探索一下KVC的底層原理。
KVC定義及API
KVC(Key-Value Coding)
是利用NSKeyValueCoding
非正式協議實現的一種機制,對象采用這種機制來提供對其屬性的間接訪問。
NSKeyValueCoding
在Foundation
框架下:
-
KVC
是通過對NSObject
的擴展來實現的 —— 只要繼承了NSObject
的類都可以使用KVC
-
NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet
等也遵守KVC
協議 -
除少數類型(結構體)以外都可以使用
KVC
KVC常用方法:
// 通過 key 設值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 通過 key 取值
- (nullable id)valueForKey:(NSString *)key;
// 通過 keyPath 設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 通過 keyPath 取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
NSKeyValueCoding
類別的其它方法:
// 默認為YES。 如果返回為YES,如果沒有找到 set<Key> 方法的話, 會按照_key, _isKey, key, isKey的順序搜索成員變量, 返回NO則不會搜索
+ (BOOL)accessInstanceVariablesDirectly;
// 鍵值驗證, 可以通過該方法檢驗鍵值的正確性, 然后做出相應的處理
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 如果key不存在, 并且沒有搜索到和key有關的字段, 會調用此方法, 默認拋出異常。兩個方法分別對應 get 和 set 的情況
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// setValue方法傳 nil 時調用的方法
// 注意文檔說明: 當且僅當 NSNumber 和 NSValue 類型時才會調用此方法
- (void)setNilValueForKey:(NSString *)key;
// 一組 key對應的value, 將其轉成字典返回, 可用于將 Model 轉成字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
KVC的使用
關于KVC的使用,其實筆者在之前已經有過很詳細的分析了(詳情請見博客【iOS】KVC),但是這里由于要分析KVC的源碼,還是把基本的接口和用法再整理一遍
首先定義兩個類方便后續使用
基本類型
對于基本類型KVC的使用,要注意NSInteger
這類的屬性賦值時要轉成NSNumber
或NSString
打印的結果如下:
集合類型
打印結果:
訪問非對象類型——結構體
-
對于非對象類型的賦值總是把它先轉成
NSValue
類型再進行存儲 -
取值時轉成對應類型后再使用
打印結果:
集合操作符
聚合操作符
-
@avg: 返回操作對象指定屬性的平均值
-
@count: 返回操作對象指定屬性的個數
-
@max: 返回操作對象指定屬性的最大值
-
@min: 返回操作對象指定屬性的最小值
-
@sum: 返回操作對象指定屬性值之和
數組操作符
-
@distinctUnionOfObjects: 返回操作對象指定屬性的集合--去重
-
@unionOfObjects: 返回操作對象指定屬性的集合
嵌套操作符
-
@distinctUnionOfArrays: 返回操作對象(嵌套集合)指定屬性的集合--去重,返回的是 NSArray
-
@unionOfArrays: 返回操作對象(集合)指定屬性的集合
-
@distinctUnionOfSets: 返回操作對象(嵌套集合)指定屬性的集合--去重,返回的是 NSSe
層層嵌套
通過forKeyPath
對實例變量(student)進行取值賦值通過forKeyPath
對實例變量(student)進行取值賦值
打印結果:
KVC底層原理
設值過程
KVC底層其實就是一個按順序查找的過程:
-
按
set<Key>:
、_set<Key>:
順序查找對象中是否有對應的方法 -
判斷
accessInstanceVariablesDirectly
結果-
為YES時按照
_<key>
、_is<Key>
、<key>
、is<Key>
的順序查找成員變量,找到了就賦值;找不到就跳轉第3步 -
為NO時跳轉第3步
-
-
調用
setValue:forUndefinedKey:
。默認情況下會引發一個異常,但是繼承于NSObject
的子類可以重寫該方法就可以避免崩潰并做出相應措施
取值過程
取值過程是類似的流程:
-
按照
get<Key>
、<key>
、is<Key>
、_<key>
順序查找對象中是否有對應的方法 -
查找是否有
countOf<Key>
和objectIn<Key>AtIndex:
方法(對應于NSArray
類定義的原始方法)以及<key>AtIndexes:
方法(對應于NSArray
方法objectsAtIndexes:
)-
如果找到其中的第一個(
countOf<Key>
),再找到其他兩個中的至少一個,則創建一個響應所有NSArray
方法的代理集合對象,并返回該對象(即要么是countOf<Key> + objectIn<Key>AtIndex:
,要么是countOf<Key> + <key>AtIndexes:
,要么是countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:
) -
如果沒有找到,跳轉到第3步
-
-
查找名為
countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>
這三個方法(對應于NSSet
類定義的原始方法)-
如果找到這三個方法,則創建一個響應所有NSSet方法的代理集合對象,并返回該對象
-
如果沒有找到,跳轉到第4步
-
-
判斷
accessInstanceVariablesDirectly
,為YES時按照_<key>
、_is<Key>
、<key>
、is<Key>
的順序查找成員變量,找到了就取值 -
判斷取出的屬性值
-
屬性值是對象,直接返回
-
屬性值不是對象,但是可以轉化為
NSNumber
類型,則將屬性值轉化為NSNumber
類型返回 -
屬性值不是對象,也不能轉化為
NSNumber
類型,則將屬性值轉化為NSValue
類型返回
-
-
調用
valueForUndefinedKey:
.默認情況下會引發一個異常,但是繼承于NSObject
的子類可以重寫該方法就可以避免崩潰并做出相應措施
自定義KVC
我們可以自定義KVC
setter方法
首先在頭文件中加入這個方法,在.m文件中引入<objc/runtime.h>這個庫
然后開始實現流程,大致流程如下:
- (void)cj_setValue:(nullable id)value forKey:(NSString *)key {// 1:非空判斷一下if (key == nil || key.length == 0) return;// 2:找到相關方法 set<Key> _set<Key> setIs<Key>// 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 cj_performSelectorWithMethodName:setKey value:value]) {NSLog(@"*********%@**********",setKey);return;} else if ([self cj_performSelectorWithMethodName:_setKey value:value]) {NSLog(@"*********%@**********",_setKey);return;} else if ([self cj_performSelectorWithMethodName:setIsKey value:value]) {NSLog(@"*********%@**********",setIsKey);return;}NSString *undefinedMethodName = @"setValue:forUndefinedKey:";IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));// 3:判斷是否能夠直接賦值實例變量if (![self.class accessInstanceVariablesDirectly]) {if (undefinedIMP) {[self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];} else {@throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];}return;}// 4.找相關實例變量進行賦值// 4.1 定義一個收集實例變量的可變數組NSMutableArray *mArray = [self getIvarListName];NSString *_key = [NSString stringWithFormat:@"_%@",key];NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];NSString *isKey = [NSString stringWithFormat:@"is%@",Key];// _<key> _is<Key> <key> is<Key>if ([mArray containsObject:_key]) {// 4.2 獲取相應的 ivarIvar 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:如果找不到相關實例if (undefinedIMP) {[self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];} else {@throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];}
}
getter方法
第一步是加入庫和聲明方法,和setter方法相同,實現方法的過程如下:
- (nullable id)cj_valueForKey:(NSString *)key {// 1:刷選key 判斷非空if (key == nil ?|| key.length == 0) return nil;
?// 2:找到相關方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex// key 要大寫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 popNSString *undefinedMethodName = @"valueForUndefinedKey:";IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));// 3:判斷是否能夠直接賦值實例變量if (![self.class accessInstanceVariablesDirectly]) {if (undefinedIMP) {
?
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
#pragma clang diagnostic pop} else {@throw [NSException exceptionWithName:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];}}// 4.找相關實例變量進行賦值// 4.1 定義一個收集實例變量的可變數組NSMutableArray *mArray = [self getIvarListName];// _<key> _is<Key> <key> is<Key>// _name -> _isName -> name -> isNameNSString *_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);;}
?if (undefinedIMP) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
#pragma clang diagnostic pop} else {@throw [NSException exceptionWithName:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];}
?return nil;
}
過程幾個用到的方法封裝如下://安全調用方法及傳兩個參數
- (BOOL)cj_performSelectorWithMethodName:(NSString *)methodName value:(id)value key:(id)key {if ([self respondsToSelector:NSSelectorFromString(methodName)]) {#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"[self performSelector:NSSelectorFromString(methodName) withObject:value withObject:key];
#pragma clang diagnostic popreturn YES;}return NO;
}
?
//安全調用方法及傳一個參數
- (BOOL)cj_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 popreturn YES;}return NO;
}
?
//安全調用方法
- (id)performSelectorWithMethodName:(NSString *)methodName {if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"return [self performSelector:NSSelectorFromString(methodName)];
#pragma clang diagnostic pop}return nil;
}
?
//取成員變量
- (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;
}
KVC異常小技巧
自動轉換類型
-
用
int
類型賦值會自動轉成__NSCFNumber
-
用結構體類型賦值會自動轉成
NSConcreteValue
設置空值
有時候在設值時設置空值,可以通過重寫setNilValueForKey
來監聽,但是setNilValueForKey
只對NSNumber
類型有效
// Int類型設置nil
[person setValue:nil forKey:@"age"];
// NSString類型設置nil
[person setValue:nil forKey:@"subject"];
?
@implementation TCJPerson
?
- (void)setNilValueForKey:(NSString *)key {NSLog(@"設置 %@ 是空值", key);
}
?
@end
?
未定義的key
未定義的key可以用setValue:forUndefinedKey:
、valueForUndefinedKey:
來監聽