iOS KVC底層原理

KVC全稱Key-Value Coding,俗稱鍵值編碼掸掏,是由NSKeyValueCoding非正式協(xié)議啟用的一種機制茁影,對象采用該協(xié)議可以間接訪問其屬性,可以通過一個字符串Key來訪問某個屬性丧凤。這種間接訪問機制補充了是咧變量及其相關(guān)的訪問器方法所提供的直接訪問募闲。

相關(guān)API

  • 通過Key取值和設值
//直接通過Key來取值
- (nullable id)valueForKey:(NSString *)key;

//通過Key來設值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
  • 通過keyPath(路由)取值和設值
//通過KeyPath來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; 

//通過KeyPath來設值                 
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

  • 其他方法
//默認返回YES,表示如果沒有找到Set<Key>方法的話息裸,會按照_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有關(guān)的字段或者屬性格了,則會調(diào)用這個方法,默認是拋出異常徽鼎。
- (nullable id)valueForUndefinedKey:(NSString *)key;

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

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

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

KVC底層原理

通過setValue:forKey方法聲明檐嚣,我們發(fā)現(xiàn)是在Foundation框架中,由于Foundation是不開源的啰扛,我們可以通過下面幾種方式來探索

  • 通過Hopper反匯編嚎京,查看偽代碼
  • Github搜索相關(guān)文檔
  • 通過蘋果官方文檔

下面我們通過官方文檔來查看KVC的底層原理

KVC設值流程

  • 【第一步】查找三種setter方法,查找順序是set<Key> --> _set<Key> --> setIs<Key>

    • 如果包含其中任意一個侠讯,直接設值屬性value(key是指成員變量名挖藏,首字符大小寫需要符合KVC的命名規(guī)范
    • 如果沒有,進入【第二步】
  • 【第二步】判斷accessInstanceVariablesDirectly是否返回YES

    • 如果返回YES厢漩,則查找實例變量進行賦值膜眠,查找順序是_<Key> --> _is<Key> --> <Key> -- > is<Key>
      • 如果找到任意一個就進行賦值
      • 如果沒有找到進入【第三步】
  • 【第三步】如果setter方法實例變量都沒找到,會執(zhí)行setValue:forUndefinedKey:,默認拋出NSUndefinedKeyException異常

    kvc設置流程

KVC取值流程

  • 【第一步】查找getter方法宵膨,查找順序是get<Key> --> <Key> --> is<Key> --> _<Key>

    • 找到架谎,執(zhí)行【第五步】
    • 沒有找到,執(zhí)行【第二步】
  • 【第二步】在實例中搜索countOf<Key>辟躏、objectIn<Key>AtIndex:谷扣、objectsAtIndexes :

    • 如果找到countOf<Key>和其余任意一個,創(chuàng)建一個響應所有NSArray方法集合代理對象并返回捎琐,然后代理對象將收到的所有NSArray消息轉(zhuǎn)化成countOf<Key>会涎,objectIn<Key>AtIndex:和<key>AtIndexes:消息的組合,用來創(chuàng)建鍵值編碼對象瑞凑,如果原始對象還實現(xiàn)了get<Key>:range:之類的可選方法末秃,則代理對象也將在適當?shù)臅r候使用該方法,實際上籽御,代理對象與與鍵值編碼兼容的對象一起使用练慕,可以使基礎屬性的行為就好像它是NSArray,即使不是技掏。(注意:方法名的命名規(guī)則要符合KVC的標準命名方法铃将,包括方法簽名。)

    • 沒找到哑梳,執(zhí)行【第三步】

  • 【第三步】同時查找countOf <Key>劲阎,enumeratorOf<Key>和memberOf<Key>這三個方法

    • 如果三個方法都找到,創(chuàng)建一個響應所有NSSet方法的集合代理對象并將其返回
    • 沒有找到鸠真,執(zhí)行【第四步】
  • 【第四步】檢測類方法InstanceVariablesDirectly是否為YES哪工,依次搜索_<key>,_is<Key>弧哎,<key>或is<Key>的實例變量

    • 如果找到,直接獲取實例變量的值稚虎,執(zhí)行【第五步】
    • 沒有找到撤嫩,執(zhí)行【第六步】
  • 【第六步】執(zhí)行對象方法valueForUndefinedKey :,默認拋出NSUndefinedKeyException的異常

    KVC取值流程

KVC使用場景

1蠢终、動態(tài)設值和取值

  • 通過setValue:forKey:valueForKey:
  • 通過路由的方式setValue:forKeyPath:valueForKeyPath:

2序攘、訪問和修改私有變量

對于類的私有屬性,在外部定義的對象寻拂,是無法直接訪問私有屬性的程奠,但是對于KVC而言,一個對象沒有自己的隱私祭钉,可以通過KVC修改和訪問任何私有屬性

3瞄沙、多值操作(model和字典互轉(zhuǎn))

model和字典的轉(zhuǎn)換可以通過下面兩個KVC的API實現(xiàn)

//字典轉(zhuǎn)模型
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

//模型轉(zhuǎn)字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

4、修改一些系統(tǒng)的內(nèi)部屬性

很多UI控件并沒有提供訪問的API,但是使用KVC可以解決這個問題距境,自定義tabbar申尼、個性化UITextField中placeHolderText

5、高階消息傳遞

在對容器類使用KVC時垫桂,valueForKey:將會被傳遞給容器中的每一個對象师幕,而不是對容器本身進行操作,結(jié)果會被添加到返回的容器中诬滩,這樣霹粥,可以很方便的操作集合 來返回 另一個集合

//KVC實現(xiàn)高階消息傳遞
- (void)transmitMsg{
    NSArray *arrStr = @[@"english", @"franch", @"chinese"];
    NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];//首字母大寫
    
    for (NSString *str in arrCapStr) {
        NSLog(@"%@", str);
    }
    
    NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"];
    for (NSNumber *length in arrCapStrLength) {
        NSLog(@"%ld", (long)length.integerValue);
    }
}

//********打印結(jié)果********
2020-10-27 11:33:43.377672+0800 CJLCustom[60035:6380757] English
2020-10-27 11:33:43.377773+0800 CJLCustom[60035:6380757] Franch
2020-10-27 11:33:43.377860+0800 CJLCustom[60035:6380757] Chinese
2020-10-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 7
2020-10-27 11:33:43.378327+0800 CJLCustom[60035:6380757] 6
2020-10-27 11:33:43.378417+0800 CJLCustom[60035:6380757] 7

自定義KVC

自定義KVC設值

  • 1、判斷key是否為空
  • 2疼鸟、查找setter方法后控,set<Key>、_set<Key>愚臀、 setIs<Key>
  • 3忆蚀、判斷是否能間接訪問實例變量,即是否響應accessInstanceVariablesDirectly方法姑裂,
    • YES 繼續(xù)下一步
    • NO 崩潰報錯
  • 4馋袜、間接訪問變量賦值(只會走一次)_key、_isKey舶斧、key欣鳖、isKey
    • 4.1、定義一個收集實例變量的集合
    • 4.2茴厉、class_getInstanceVariable獲取對應的ivar(實例變量)
    • 4.3泽台、object_setIvar,設置對應的ivar(實例變量)值
  • 5矾缓、如果找不到相關(guān)實例變量怀酷,則拋出異常

自定義KVC取值

  • 1、判斷Key空值
  • 2嗜闻、查找對應的方法get<kKey>蜕依、<Key>、countof<Key>琉雳、objectIn<Key>AtIndex
  • 3样眠、判斷是否能間接賦值實例變量,即判斷是否響應accessInstanceVariablesDirectly方法
    • YES 繼續(xù)下一步
    • NO 崩潰報錯
  • 4翠肘、間接訪問實例變量檐束,_<key> _is<Key> <key> is<Key>
    • 4.1 定義一個收集實例變量的可變數(shù)組
    • 4.2 class_getInstanceVariable,獲取相應的 ivar
    • 4.3 object_getIvar束倍,返回相應的 ivar 的值

keyPath -- 路由訪問

//通過KeyPath來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  

//通過KeyPath來設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

//CJLPerson類
@interface CJLPerson : NSObject
@property (nonatomic, copy)   NSString          *age;
@property (nonatomic, strong) CJLStudent         *student;
@end

//CJLStudent類
@interface CJLStudent : NSObject
@property (nonatomic, copy)   NSString          *name;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        CJLPerson *person = [[CJLPerson alloc] init];
        CJLStudent *student = [CJLStudent alloc];
        student.name    = @"CJL";
        person.student     = student;
        //根據(jù)kvc - keyPath路由修改student的subject屬性的值
        [person setValue:@"嘻嘻" forKeyPath:@"student.name"];
        NSLog(@"%@",[person valueForKeyPath:@"student.name"]);
    }
    return 0;
}

//*************打印結(jié)果*************
2020-10-27 09:55:08.512833+0800 001-KVC簡介[58089:6301894] 改變前:CJL
2020-10-27 09:55:08.512929+0800 001-KVC簡介[58089:6301894] 改變后:嘻嘻

自定義KVC代碼

#import "NSObject+YPKVC.h"
#import <objc/runtime.h>

@implementation NSObject (YPKVC)

//設值
- (void)yp_setValue:(nullable id)value forKey:(NSString *)key{
    
//    1被丧、判斷key 是否存在
    if (key == nil || key.length == 0) return;
    
//    2盟戏、找setter方法,順序是:setXXX晚碾、_setXXX抓半、 setIsXXX
    // key 要大寫
    NSString *Key = key.capitalizedString;
    // key 要大寫
    NSString *setKey = [NSString stringWithFormat:@"set%@:", Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key];
    
    if ([self yp_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*************%@*************", setKey);
        return;
    }else if([self yp_performSelectorWithMethodName:_setKey value:value]){
        NSLog(@"*************%@*************", _setKey);
        return;
    }else if([self yp_performSelectorWithMethodName:setIsKey value:value]){
        NSLog(@"*************%@*************", setIsKey);
        return;
    }
    
    
//    3、判斷是否響應`accessInstanceVariablesDirectly`方法格嘁,即間接訪問實例變量笛求,返回YES,繼續(xù)下一步設值糕簿,如果是NO探入,則崩潰
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"YPUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
//    4、間接訪問變量賦值懂诗,順序為:_key蜂嗽、_isKey、key殃恒、isKey
    // 4.1 定義一個收集實例變量的可變數(shù)組
    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:@"YPUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
    
}
//取值
- (nullable id)yp_valueForKey:(NSString *)key{
    
//    1、判斷非空
    if (key == nil || key.length == 0) {
        return nil;
    }
    
//    2离唐、找到相關(guān)方法: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病附、判斷是否響應`accessInstanceVariablesDirectly`方法,即間接訪問實例變量完沪,返回YES熟呛,繼續(xù)下一步設值,如果是NO馋没,則崩潰
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"YPUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
//    4.找相關(guān)實例變量進行賦值婆排,順序為:_<key>鉴扫、 _is<Key>、 <key>、 is<Key>
    // 4.1 定義一個收集實例變量的可變數(shù)組
    NSMutableArray *mArray = [self getIvarListName];
    // 例如:_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 @"";
    
    return @"";
}

#pragma mark - 相關(guān)方法
- (BOOL)yp_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
        //如果你確定不會發(fā)生內(nèi)存泄漏的情況下姥饰,可以使用如下的語句來忽略掉這條警告
#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
        //如果你確定不會發(fā)生內(nèi)存泄漏的情況下岂座,可以使用如下的語句來忽略掉這條警告
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}

- (NSMutableArray *)getIvarListName{
    //創(chuàng)建可變數(shù)組钾恢,用于存儲ivar成員變量
    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);
        //將字符轉(zhuǎn)換成字符串
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@", ivarName);
        //存入可變數(shù)組
        [mArray addObject:ivarName];
    }
    //釋放成員變量列表
    free(ivars);
    return mArray;
}

@end
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鸳址,隨后出現(xiàn)的幾起案子瘩蚪,更是在濱河造成了極大的恐慌,老刑警劉巖稿黍,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疹瘦,死亡現(xiàn)場離奇詭異,居然都是意外死亡巡球,警方通過查閱死者的電腦和手機言沐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門邓嘹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人险胰,你說我怎么就攤上這事汹押。” “怎么了起便?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵棚贾,是天一觀的道長。 經(jīng)常有香客問我缨睡,道長鸟悴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任奖年,我火速辦了婚禮细诸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘陋守。我一直安慰自己震贵,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布水评。 她就那樣靜靜地躺著猩系,像睡著了一般。 火紅的嫁衣襯著肌膚如雪中燥。 梳的紋絲不亂的頭發(fā)上寇甸,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音疗涉,去河邊找鬼拿霉。 笑死,一個胖子當著我的面吹牛咱扣,可吹牛的內(nèi)容都是我干的绽淘。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼闹伪,長吁一口氣:“原來是場噩夢啊……” “哼沪铭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起偏瓤,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤杀怠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后厅克,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赔退,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年已骇,在試婚紗的時候發(fā)現(xiàn)自己被綠了离钝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡褪储,死狀恐怖卵渴,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鲤竹,我是刑警寧澤浪读,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站辛藻,受9級特大地震影響碘橘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吱肌,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一痘拆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧氮墨,春花似錦纺蛆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至猛铅,卻和暖如春字支,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背奸忽。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工堕伪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人月杉。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓十绑,卻偏偏與公主長得像瞬欧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容