iOS底層學習 - KVC探索之路

KVC是咱們在平常開發中經常使用的功能,俗稱'鍵值編碼',本章節就來探索一下咱們經常使用的KVC究竟是如何實現的html

什麼是KVC

基本定義

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

  • KVC是一種對對象屬性的間接訪問
  • 通常經過getset方法的監聽來進行取值和設值
  • 是一種基本的編程思想

基本用法

基本的用法分爲兩種:賦值和取值。主要API以下數組

賦值

//賦值
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
複製代碼

取值

//取值
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key; 
複製代碼

KVC的幾種使用方法

首先先創建一個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屬性,定義爲publicmyName成員變量和基本類型intage屬性,直接使用上述的基本用法賦值便可,代碼以下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:方法經過引用接收值和錯誤參數,因此會有如下三種結果:

  1. 驗證方法認爲值對象有效,並在YES不更改值或錯誤的狀況下返回。

  2. 驗證方法認爲值對象無效,但選擇不對其進行更改。在這種狀況下,該方法返回NO錯誤參考並將錯誤參考(若是由調用者提供)設置到一個NSError指示失敗緣由的對象。

  3. 驗證方法認爲值對象無效,但建立了一個新的有效對象做爲替換。在這種狀況下,該方法返回,YES而錯誤對象保持不變。在返回以前,該方法將值引用修改成指向新值對象。進行修改時,即便值對象是可變的,該方法也老是建立一個新對象,而不是修改舊對象。

相關的代碼以下

Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
    NSLog(@"%@",error);
}
複製代碼

KVC 的底層實現原理(搜索模式)

經過官方文檔,咱們能夠很清晰的知道KVC在底層是如何進行setget

setValue:forKey:方法

  1. 按照順序查找,是否存在set<Key>_set<Key>setIs<Key>的方法。若是存在,則直接進行調用
  2. 若是沒有條件1中的方法。則優先判斷accessInstanceVariablesDirectly方法是否爲YES(改方法系統默認爲YES,用來判斷是都容許對成員變量的賦值)。若是爲YES。則按照順序,查找成員變量_Key_isKeyKeyisKey,若是存在對應的成員變量,則直接進行賦值,若是不存在,則進行下一步3.
  3. 若是1和2中的條件均不知足,則會setValue:forUndefinedKey:報錯

圖解以下

getValue:forKey:方法

  1. get<Key>, <key>, is<Key> 以及 _<key>的順序查找對象中是否有對應的方法。

    • 若是找到了,將方法返回值帶上跳轉到第 5 步
    • 若是沒有找到,跳轉到第 2 步
  2. 查找是否有 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 步
  3. 查找名爲 countOf<Key>enumeratorOf<Key>memberOf<Key> 這三個方法(對應於NSSet類定義的原始方法)

    • 若是找到這三個方法,則建立一個響應全部 NSSet 方法的代理集合對象,並返回該對象
    • 若是沒有找到,跳轉到第 4 步
  4. 判斷類方法 accessInstanceVariablesDirectly 結果

    • 若是返回 YES,則以_<key>, _is<Key>, <key>, is<Key> 的順序查找成員變量,若是找到了,將成員變量帶上跳轉到第 5 步,若是沒有找到則跳轉到第 6 步
    • 若是返回 NO,跳轉到第 6 步
  5. 判斷取出的屬性值

    • 若是屬性值是對象,直接返回
    • 若是屬性值不是對象,可是能夠轉化爲 NSNumber 類型,則將屬性值轉化爲 NSNumber 類型返回
    • 若是屬性值不是對象,也不能轉化爲 NSNumber 類型,則將屬性值轉化爲 NSValue 類型返回
  6. 調用 valueForUndefinedKey:。 默認狀況下,這會引起一個異常,可是NSObject 的子類能夠提供特定於 key 的行爲。

圖解以下:

自定義KVC

瞭解了KVC的基本使用和底層原理以後,咱們能夠根據其底層原理,來實現一個簡單的自定義KVC。

首先咱們建立一個NSObject的分類並添加前綴自定義方法名,用來解耦和避免和系統方法衝突

自定義賦值

根據上面的小結,主體思路以下:

  1. 非空判斷
  2. 找到相關方法set<Key>_set<Key>setIs<Key>
  3. 判斷是否可以直接賦值實例變量
  4. 找相關實例變量進行賦值
  5. 若是找不到相關實例,拋出異常

相關代碼以下:

- (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];
}
複製代碼

自定義取值

根據上面的小結,主體思路以下:

  1. 非空判斷
  2. 找到相關方法 get<Key><key>countOf<Key>objectIn<Key>AtIndex
  3. 判斷是否可以直接賦值實例變量
  4. 找相關實例變量進行賦值

相關的代碼以下:

- (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的實現方式,來進行進一步加深印象

相關文章
相關標籤/搜索