iOS KVC底層原理分析

準備工作

KVC協議定義

KVCNSKeyValueCoding的簡寫,鍵值編碼是由NSKeyValueCoding非正式協議啟用的一種機制,對象采用該機制來提供對其屬性的間接訪問。當對象符合鍵值編碼時,其屬性可通過字符串參數通過簡潔、統一的消息傳遞接口進行尋址。這種間接訪問機制補充了實例變量及其相關訪問器方法提供的直接訪問。

KVC在Objective-C中的定義

KVC的定義都是對NSObject的擴展來實現的,查看setValueForKey方法,發現其在Foundation里面,而Foundation框架是不開源的,只能在蘋果官方文檔查找。見下圖:

Foundation框架

KVC提供的API方法

  • 我們可以通過官方提供的文檔進行查看(文章開頭有鏈接)
  • 蘋果對一些容器類比如NSArray或者NSSet等,KVC有著特殊的實現。

常用方法

對于所有繼承了NSObject的類型,也就是幾乎所有的Objective-C對象都能使用KVC,下面是KVC最為重要的四個方法:

   - (nullable id)valueForKey:(NSString *)key;                          // 直接通過Key來取值
   - (void)setValue:(nullable id)value forKey:(NSString *)key;          // 通過Key來設值
   - (nullable id)valueForKeyPath:(NSString *)keyPath;                  // 通過KeyPath來取值
   - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  // 通過KeyPath來設值

特殊方法

NSKeyValueCoding類別中還有其他的方法,當我們遇到適合的需求時,就能夠派上用場了。方法如下:

// 默認返回YES,表示如果沒有找到Set方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設置成NO就不這樣搜索
+ (BOOL)accessInstanceVariablesDirectly;

// KVC提供屬性值正確性驗證的API,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設置新值并返回錯誤原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

// 這是集合操作的API,里面還有一系列這樣的API,如果屬性是一個NSMutableArray,那么可以用這個方法來返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// 如果Key不存在,且沒有KVC無法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常。
- (nullable id)valueForUndefinedKey:(NSString *)key;

// 和上一個方法一樣,但這個方法是設值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

// 如果你在SetValue方法時面給Value傳nil,則會調用這個方法
- (void)setNilValueForKey:(NSString *)key;

// 輸入一組key,返回該組key對應的Value,再轉成字典返回,用于將Model轉到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

KVC常用案例

  • 結構體的處理
    KVC在進行結構體處理時,需要用到NSValue,設值時,將結構體封裝成NSValue,進行鍵值設值;取值同樣返回NSValue,然后按照結構體格式進行解析,見下面代碼:
    // 結構體
    ThreeFloats floats = {1.,2.,3.};
    // 封裝成NSValue
    NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    // 設值
    [person setValue:value forKey:@"threeFloats"];

    // 取值
    NSValue *value1    = [person valueForKey:@"threeFloats"];
    // 結構體解析
    ThreeFloats th;
    [value1 getValue:&th];
    NSLog(@"%f-%f-%f",th.x,th.y,th.z);
  • 字典處理(模型轉換)
    字典可以實現與模型進行裝換,也可以通過鍵值數組從模型中獲取字典數據。實現代碼如下:
- (void)dictionaryTest{
    // 字典
    NSDictionary* dict = @{
                           @"name":@"Cooci",
                           @"nick":@"KC",
                           @"subject":@"iOS",
                           @"age":@18,
                           @"length":@180
                           };
    // 模型
    LGStudent *p = [[LGStudent alloc] init];
    // 字典轉模型
    [p setValuesForKeysWithDictionary:dict];

    // 鍵值數組
    NSArray *array = @[@"name",@"age"];
    // 從模型中獲取響應的字典數據
    NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
    NSLog(@"%@",dic);
}

KVC設值取值順序

KVC的使用相信是沒什么難度的,但是它尋找key的過程是怎么樣子的呢?以下就進行分析。

設值

當調用setValue:forKey:代碼時,會有什么的內部操作呢?我在其官方文章中找到下圖:

官方解釋

上圖的意思是:
setValue:forKey:的默認實現,給定keyvalue參數作為輸入,嘗試將名為key的屬性設置為value,在接收調用的對象內部,使用以下過程:按順序查找名為 set<Key>:_set<Key> 的第一個訪問器。 如果找到,則使用輸入值(或根據需要展開的值)調用它并完成。如果未找到簡單訪問器,并且類方法 accessInstanceVariablesDirectly返回 YES,則按順序查找名稱類似于 _<key>_is<Key><key>is<Key> 的實例變量。 如果找到,直接使用輸入值(或解包值)設置變量并完成。
在未找到訪問器或實例變量時,調用 setValue:forUndefinedKey:。 默認情況下,這會引發異常,但 NSObject的子類可能會提供特定于鍵的行為。

根據上面的解析可以總結為以下的幾點:

  • 按順序查找名為set<Key>_set<Key>或者setIs<Key>setter訪問器順序查找,如果找到就調用。只要實現任意一個,那么就會將調用這個方法,將屬性的值設為傳進來的值
  • 如果沒有找到這些setter方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,如果重寫了該方法讓其返回NO的話,那么在這一步KVC會執行setValue:forUndefinedKey:方法。
  • 如果返回YESKVC機制會優先搜索該類里面有沒有名為_<Key>的成員變量,無論該變量是在類接口處定義,還是在類實現處定義,也無論用了什么樣的訪問修飾符,只在存在以_<Key>命名的變量,KVC都可以對該成員變量賦值。
  • KVC機制再會繼續搜索_is<Key><key>is<key>的成員變量,再給它們賦值。
  • 如果上面列出的方法或者成員變量都不存在,系統將會執行該對象的setValue:forUndefinedKey:方法,默認是拋出異常。
[person setValue:@"newName" forKey:@"name"];為例,得出結論:
  • 優先通過setter方法,進行屬性設置,調用順序是:
    • setName
    • _setName
    • setIsName
  • 果以上方法均未找到,并且accessInstanceVariablesDirectly返回YES,則通過成員變量進行設置,順序是:
    • _name
    • _isName
    • name
    • isName

注意:以上可以通過案例進行演示,我就不在這里演示了。

補充說明accessInstanceVariablesDirectly

嘗試重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO,如果KVC沒有找到set<Key>_set<Key>setIs<Key>相關方法時,會直接用setValue:forUndefinedKey:方法。我們用代碼來測試一下上面的KVC機制:

@interface LGPerson : NSObject
{
    @public
        NSString *_isName;
        NSString *name;
        NSString *isName;
        NSString *_name;
}
@end
@implementation LGPerson

+(BOOL)accessInstanceVariablesDirectly{
    return NO;
}

-(id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"出現異常,該key不存在%@",key);
    return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
     NSLog(@"出現異常,該key不存在%@",key);
}

// 設置方法全部注釋掉
// -(void)setName:(NSString*)name{
//     toSetName = name;
// }
// - (void)_setName:(NSString *)name{
//     NSLog(@"%s - %@",__func__,name);
// }
// - (void)setIsName:(NSString *)name{
//     NSLog(@"%s - %@",__func__,name);
// }


@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson* person = [LGPerson new];
        [person setValue:@"NewName" forKey:@"name"];
        NSString* name = [person valueForKey:@"name"];
        NSLog(@"value for key : %@",name);

        NSLog(@"取值_name:%@",person->_name);
        NSLog(@"取值_isName:%@",person->_isName);
        NSLog(@"取值name:%@",person->name);
        NSLog(@"取值isName:%@",person->isName);
    }
    return 0;
}

運行結果:


運行結果

這說明了重寫+(BOOL)accessInstanceVariablesDirectly方法讓其返回NO后,KVC找不到set<Key>等方法后,不再去找<Key>系列成員變量,而是直接調用setValue:forUndefinedKey:,如果我們自身的類不需要KVC機制的話可以這樣子寫。

KVC設值流程圖
KVC設值流程圖

取值

同理,在調用valueForKey:時候會發生什么呢?根據官方的文檔得出:

valueForKey官方文檔描述

根據官方文檔得出valueForKey:的機制如下:

  • 首先按get<Key><Key>is<Key>_<Key>的順序方法查找getter方法,找到的話會直接調用,如果是BOOL或者Int等值類型, 會將其包裝成一個NSNumber對象。
  • 如果上面的getter沒有找到,KVC則會查找countOf<Key>objectIn<Key>AtIndex<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外兩個方法中的一個被找到,那么就會返回一個可以響應NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調用這個代理集合的方法,或者說給這個代理集合發送屬于NSArray的方法,就會以countOf<Key>objectIn<Key>AtIndexAt<Key>Indexes這幾個方法組合的形式調用。還有一個可選的get<Key>:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名
  • 如果上面的方法沒有找到,那么會同時查找countOf<Key>enumeratorOf<Key>memberOf<Key>格式的方法。如果這三個方法都找到,那么就返回一個可以響應NSSet所的方法的代理集合,和上面一樣,給這個代理集合發NSSet的消息,就會以countOf<Key>enumeratorOf<Key>memberOf<Key>組合的形式調用。
  • 如果還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),那么和先前的設值一樣,會按_<Key>_is<Key><Key>is<Key>的順序搜索成員變量名,這里不推薦這么做,因為這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那么會直接調用valueForUndefinedKey:
  • 還沒有找到的話,調用valueForUndefinedKey:
[person valueForKey:@"name"];為例
  • getter方法的調用順序是:
    • getName
    • name
    • isName
    • _name
  • 如果以上方法沒有找到,accessInstanceVariablesDirectly返回YES,則直接返回成員變量,獲取順序依然是:
    • _name
    • _isName
    • name
    • isName

注意:以上可以通過案例進行演示,我就不在這里演示了。

KVC取值流程圖
KVC取值流程
代碼驗證KVC取值(需要的話就拷貝運行即可)
    @interface LGPerson : NSObject
    {
        @public
            NSString *_isName;
            NSString *name;
            NSString *isName;
            NSString *_name;
    }
    @end
    @implementation LGPerson

    +(BOOL)accessInstanceVariablesDirectly{
        return NO;
    }

    -(id)valueForUndefinedKey:(NSString *)key{
        NSLog(@"出現異常,該key不存在%@",key);
        return nil;
    }
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
         NSLog(@"出現異常,該key不存在%@",key);
    }

    // 設置方法全部注釋掉
    // -(void)setName:(NSString*)name{
    //     toSetName = name;
    // }
    // - (void)_setName:(NSString *)name{
    //     NSLog(@"%s - %@",__func__,name);
    // }
    // - (void)setIsName:(NSString *)name{
    //     NSLog(@"%s - %@",__func__,name);
    // }

    // 取值方法
    //- (NSString *)getName{
    //    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)name{
    //    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)isName{
    //    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)_name{
    //    return NSStringFromSelector(_cmd);
    //}
    @end

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            LGPerson* person = [LGPerson new];
            [person setValue:@"NewName" forKey:@"name"];
            NSString* name = [person valueForKey:@"name"];
            NSLog(@"value for key : %@",name);

            NSLog(@"取值_name:%@",person->_name);
            NSLog(@"取值_isName:%@",person->_isName);
            NSLog(@"取值name:%@",person->name);
            NSLog(@"取值isName:%@",person->isName);
        }
        return 0;
    }

在KVC中使用keyPath

除了對當前對象的屬性進行賦值外,還可以對其更深層的對象進行賦值。例如,對當前對象的location屬性的country屬性進行賦值。KVC進行多級訪問時,直接類似于屬性調用一樣用點語法進行訪問即可。

    [person setValue:@"" forKeyPath:@"location.country"];

通過keyPath對數組進行取值時,并且數組中存儲的對象類型都相同,可以通過valueForKeyPath:方法指定取出數組中所有對象的某個字段。例如下面例子中,通過valueForKeyPath:將數組中所有對象的name屬性值取出,并放入一個數組中返回。

    NSArray *names = [array valueForKeyPath:@"name"];
例子展示以及運行結果

異常處理

當根據KVC搜索規則,沒有搜索到對應的key或者keyPath,則會調用對應的異常方法。異常方法的默認實現,在異常發生時會拋出一個異常,并且應用程序Crash。見下圖:

crash現象

重寫以下兩個方法,防止crash發生:

    -(id)valueForUndefinedKey:(NSString *)key{
        NSLog(@"出現異常,該key不存在%@",key);
        return nil;
    }
    
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
         NSLog(@"出現異常,該key不存在%@",key);
    }

再次運行程序,發現不再崩潰:

再次運行程序

為了合理處理KVC發出的異常,我們還可以這樣子處理:

- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        [self setValue:@"" forKey:@”age”];
    } else {
        [super setNilValueForKey:key];
    }
}

自定義KVC的實現

根據蘋果官方文檔提供的設值、取值規則,我們可以自己進行KVC的自定義實現。見下面實現代碼:

// KVC 自定義
@implementation NSObject (LGKVC)

// 設置
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
    // 1: 判斷什么 key
    if (key == nil || key.length == 0) {
        return;
    }

    // 2: setter set<Key>: or _set<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: 判斷是否響應 accessInstanceVariablesDirectly 返回YES NO 奔潰
    // 3:判斷是否能夠直接賦值實例變量——NO
    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: 間接變量
    // 獲取 ivar -> 遍歷 containsObjct -
    // 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];
}

// 取值
- (nullable id)lg_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 pop


    // 3:判斷是否能夠直接賦值實例變量-YES、NO
    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 @"";
}

#pragma mark **- 相關方法**

- (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;
}

@end
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 什么是KVC? KVC的全稱叫Key-Value Coding,也叫做鍵值編碼,在apple官方文檔中是這么解釋的...
    Joker_King閱讀 685評論 0 3
  • YYModel的作用就是字典轉模型,在了解YYModel前,我們先了解下KVC的知識。 KVC:也稱之鍵值編碼,是...
    我叫Vincent閱讀 1,720評論 0 3
  • KVC(Key-value coding)鍵值編碼,就是指iOS的開發中,可以允許開發者通過Key名直接訪問對象的...
    奮斗的郅博閱讀 173評論 0 0
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,205評論 30 471
  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數的可能。 ...
    yichen大刀閱讀 6,098評論 0 4