KVC是咱們在平常開發中經常使用的功能,俗稱'鍵值編碼',本章節就來探索一下咱們經常使用的KVC究竟是如何實現的html
KVC
全稱是Key-Value Coding
,鍵值編碼,能夠經過Key來訪問和修改屬性。ios
咱們能夠經過查看Apple的官方文檔來查看其定義和具體的用法。git
Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.github
[譯]鍵值編碼是由
NSKeyValueCoding
非正式協議啓用的一種機制,對象採用這種機制來提供對其屬性的間接訪問。當對象符合鍵值編碼時,能夠經過簡潔,統一的消息傳遞接口經過字符串參數來訪問其屬性。這種間接訪問機制補充了實例變量及其關聯的訪問器方法提供的直接訪問。編程
經過文檔,咱們知道了以下幾點:api
get
和set
方法的監聽來進行取值和設值基本的用法分爲兩種:賦值和取值。主要API以下數組
//賦值 - (void)setValue:(id)value forKeyPath:(NSString *)keyPath; - (void)setValue:(id)value forKey:(NSString *)key; 複製代碼
//取值
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
複製代碼
首先先創建一個LGPerson
類,經過對該類各不一樣類型的屬性的賦值來說解,代碼以下bash
typedef struct { float x, y, z; } ThreeFloats; @interface LGPerson : NSObject{ @public NSString *myName; } @property (nonatomic, copy) NSString *name; @property (nonatomic, strong) NSArray *array; @property (nonatomic, strong) NSMutableArray *mArray; @property (nonatomic, assign) int age; @property (nonatomic) ThreeFloats threeFloats; @property (nonatomic, strong) LGStudent *student; @end 複製代碼
基本對象的使用是平時開發中最爲經常使用的,就如類中的name
屬性,定義爲public
的myName
成員變量和基本類型int
的age
屬性,直接使用上述的基本用法賦值便可,代碼以下markdown
[person setValue:@"WY" forKey:@"name"]; [person setValue:@19 forKey:@"age"]; [person setValue:@"WYY" forKey:@"myName"]; NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]); -------------------打印結果------------------------ WY - 19 - WYY 複製代碼
層級使用也比較容易理解,主要使用了setValue:forKeyPath:
的相關方法。好比要對類中student
屬性中的相關屬性進行賦值的時候,就不能直接進行賦值,而須要使用點語法來進行層級賦值,具體代碼以下多線程
LGStudent *student = [[LGStudent alloc] init]; student.subject = @"iOS"; person.student = student; [person setValue:@"ios" forKeyPath:@"student.subject"]; NSLog(@"%@",[person valueForKeyPath:@"student.subject"]); -------------------打印結果------------------------ ios 複製代碼
集合類型的使用即爲Person
對象中數組類型的屬性進行賦值。上述例子中,因爲屬性是不可變數組,咱們是不能直接對該屬性進行賦值操做的,通常狀況下咱們須要引入中間變量來進行等價替換,相關代碼以下
person.array = @[@"1",@"2",@"3"]; // 因爲不是可變數組 - 沒法作到 // person.array[0] = @"100"; NSArray *array = [person valueForKey:@"array"]; // 用 array 的值建立一個新的數組 array = @[@"100",@"2",@"3"]; [person setValue:array forKey:@"array"]; NSLog(@"%@",[person valueForKey:@"array"]); -------------------打印結果------------------------ ( 100, 2, 3 ) 複製代碼
能夠發現,若是使用中間變量來進行操做的話,步驟仍是相對繁瑣一些的,咱們可使用mutableArrayValueForKey
方法來達到簡化的目的,相關代碼以下
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"]; ma[0] = @"100"; NSLog(@"%@",[person valueForKey:@"array"]); -------------------打印結果------------------------ ( 100, 2, 3 ) 複製代碼
經過上述的兩種方法,咱們能夠看到使用KVC方式來對集合進行可變方法操做的讀寫更加的便捷高效,主要的方法以下
mutableArrayValueForKey:
和 mutableArrayValueForKeyPath:
返回的代理對象表現爲一個 NSMutableArray
對象
mutableSetValueForKey:
和 mutableSetValueForKeyPath:
返回的代理對象表現爲一個 NSMutableSet
對象
mutableOrderedSetValueForKey:
and mutableOrderedSetValueForKeyPath:
返回的代理對象表現爲一個 NSMutableOrderedSet
對象
這一部分在平時的開發中可能使用較少,主要是在valueForKeyPath
時,進行一些譬如求和,平均值等操做高效運算來使用的。
主要分爲如下三大類:
聚合操做符
@avg
: 返回操做對象指定屬性的平均值@count
: 返回操做對象指定屬性的個數@max
: 返回操做對象指定屬性的最大值@min
: 返回操做對象指定屬性的最小值@sum
: 返回操做對象指定屬性值之和數組操做符
@distinctUnionOfObjects
: 返回操做對象指定屬性的集合--去重@unionOfObjects
: 返回操做對象指定屬性的集合嵌套操做符
@distinctUnionOfArrays
: 返回操做對象(嵌套集合)指定屬性的集合--去重,返回的是 NSArray@unionOfArrays
: 返回操做對象(集合)指定屬性的集合@distinctUnionOfSets
: 返回操做對象(嵌套集合)指定屬性的集合--去重,返回的是 NSSet以下圖所示,當屬性爲基本標量屬性時,能夠經過NSNumber
的相關方法,轉換爲對象類型的NSNumber
來進行對應的讀寫操做
如圖所示,咱們常見的結構體的類型以下,能夠經過先轉換爲NSValue
類型的對象屬性,而後來進行對應的讀寫操做
Person
類中有一個自定義結構體的屬性,咱們對它來進行賦值看看,相關的代碼以下
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); -------------------打印結果------------------------ {length = 12, bytes = 0x0000803f0000004000004040} 1.000000 - 2.000000 - 3.000000 複製代碼
咱們能夠看到,在存儲時,咱們隊結構體進行了編碼,並生成了NSValue
的變量,進行存儲。而後又從NSValue
提取到了相對應結構體的值,完成了讀寫的流程。
KVC
支持屬性驗證,而這一特性是經過validateValue:forKey:error:
(或validateValue:forKeyPath:error:
) 方法來實現的。這個驗證方法的默認實現是去收到這個驗證消息的對象(或keyPath中最後的對象)中根據 key 查找是否有對應的 validate<Key>:error:
方法實現,若是沒有,驗證默認成功,返回 YES。
而因爲 validate<Key>:error:
方法經過引用接收值和錯誤參數,因此會有如下三種結果:
驗證方法認爲值對象有效,並在YES不更改值或錯誤的狀況下返回。
驗證方法認爲值對象無效,但選擇不對其進行更改。在這種狀況下,該方法返回N
O錯誤參考並將錯誤參考(若是由調用者提供)設置到一個NSError
指示失敗緣由的對象。
驗證方法認爲值對象無效,但建立了一個新的有效對象做爲替換。在這種狀況下,該方法返回,YES而錯誤對象保持不變。在返回以前,該方法將值引用修改成指向新值對象。進行修改時,即便值對象是可變的,該方法也老是建立一個新對象,而不是修改舊對象。
相關的代碼以下
Person* person = [[Person alloc] init]; NSError* error; NSString* name = @"John"; if (![person validateValue:&name forKey:@"name" error:&error]) { NSLog(@"%@",error); } 複製代碼
經過官方文檔,咱們能夠很清晰的知道KVC在底層是如何進行set
和get
的
set<Key>
、_set<Key>
、setIs<Key>
的方法。若是存在,則直接進行調用accessInstanceVariablesDirectly
方法是否爲YES
(改方法系統默認爲YES,用來判斷是都容許對成員變量的賦值)。若是爲YES
。則按照順序,查找成員變量_Key
、_isKey
、Key
、isKey
,若是存在對應的成員變量,則直接進行賦值,若是不存在,則進行下一步3.setValue:forUndefinedKey:
報錯圖解以下
以 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:
)查找名爲 countOf<Key>
,enumeratorOf<Key>
和 memberOf<Key>
這三個方法(對應於NSSet
類定義的原始方法)
NSSet
方法的代理集合對象,並返回該對象判斷類方法 accessInstanceVariablesDirectly
結果
YES
,則以_<key>
, _is<Key>
, <key>
, is<Key>
的順序查找成員變量,若是找到了,將成員變量帶上跳轉到第 5 步,若是沒有找到則跳轉到第 6 步NO
,跳轉到第 6 步判斷取出的屬性值
NSNumber
類型,則將屬性值轉化爲 NSNumber
類型返回NSNumber
類型,則將屬性值轉化爲 NSValue
類型返回調用 valueForUndefinedKey:
。 默認狀況下,這會引起一個異常,可是NSObject
的子類能夠提供特定於 key 的行爲。
圖解以下:
瞭解了KVC的基本使用和底層原理以後,咱們能夠根據其底層原理,來實現一個簡單的自定義KVC。
首先咱們建立一個NSObject
的分類並添加前綴自定義方法名,用來解耦和避免和系統方法衝突
根據上面的小結,主體思路以下:
set<Key>
、 _set<Key>
、setIs<Key>
相關代碼以下:
- (void)lg_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 lg_performSelectorWithMethodName:setKey value:value]) { NSLog(@"*********%@**********",setKey); return; }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) { NSLog(@"*********%@**********",_setKey); return; }else if ([self lg_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.找相關實例變量進行賦值 ✅// 4.1 定義一個收集實例變量的可變數組 NSMutableArray *mArray = [self getIvarListName]; // _<key> _is<Key> <key> is<Key> 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]; } 複製代碼
根據上面的小結,主體思路以下:
get<Key>
、 <key>
、countOf<Key>
、objectIn<Key>AtIndex
相關的代碼以下:
- (nullable id)lg_valueForKey:(NSString *)key{ ✅// 1: 判斷非空 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 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.找相關實例變量進行賦值 ✅// 4.1 定義一個收集實例變量的可變數組 NSMutableArray *mArray = [self getIvarListName]; // _<key> _is<Key> <key> is<Key> // _name -> _isName -> name -> isName 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 @""; } 複製代碼
- (BOOL)lg_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; } - (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; } 複製代碼
這裏只是很是簡單的實現,不少多線程等狀況沒有考慮,能夠參考DIS_KVC_KVO的實現方式,來進行進一步加深印象