iOS底層原理探究之----KVC

不管是平常開發(fā)還是找工作面試中,KVC碎紊、KVO的原理都是面試官比較喜歡問的問題晾蜘。最近抽時間研究了一下KVC和KVO的實現(xiàn)原理,本想著一篇文章就可以說完波桩,等研究完才發(fā)現(xiàn)不看不知道戒努,一看嚇一跳。KVC和KVO都有很多內(nèi)容可以研究镐躲,所以分為兩篇分享储玫,第一篇分享KVC的底層原理。
本次分享準備從這幾個方面入手:
1萤皂、概念定義
2撒穷、原理介紹
3、自己實現(xiàn)
4裆熙、使用場景

一端礼、概念定義

KVC:Key-value coding (鍵-值編碼)
蘋果開發(fā)者文檔中有這么一句話:


蘋果文檔

大意就是要想理解KVO必須首先理解KVC!足可見KVC的重要性入录。

概念:允許開發(fā)者通過key直接訪問對象的屬性方法或者成員變量齐媒,而不需要調(diào)用明確的存取方法。

實際上纷跛,KVC是對NSObject的擴展:NSKeyValueCoding,當然其中對NSArray邀杏、NSDictionary贫奠、NSMutableDictionary唬血、NSOrderedSet、NSSet也添加了擴展唤崭,更方便使用拷恨。
其中主要提供了以下四個方法,當然還有很多其他方法谢肾,可以在蘋果文檔中查看:

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

這些也是我們平常使用KVC使用最多的方法腕侄。

二、原理介紹

不知道大家在平常使用KVC的時候有沒有思考這些問題:
Q1:不管是set還是get芦疏,只需要傳入一個key字符串就可以存取冕杠,為什么?
Q2:如果類中沒有相應的屬性是否一定不能存人彳睢分预?
帶著這樣的問題我們研究一下KVC的實現(xiàn)原理。通過查閱蘋果的官方文檔和介紹薪捍,我們就可以了解到笼痹,在我們看似簡單的API調(diào)用下,蘋果其實是有一套完整的搜索規(guī)則酪穿,我們分set和get分別說明如下:

setValue:(id)value forKey:(NSString *)key
  • set的搜索規(guī)則如下

1.查找set<Key>:或_set<Key>命名的setter凳干,按照這個順序,如果找到的話被济,調(diào)用這個方法并將值傳進去(根據(jù)需要進行對象轉(zhuǎn)換)救赐。
2.如果沒有發(fā)現(xiàn)一個簡單的setter,但是accessInstanceVariablesDirectly類屬性返回YES溉潭,則查找一個命名規(guī)則為_<key>净响、_is<Key>、<key>喳瓣、is<Key>的實例變量馋贤。根據(jù)這個順序,如果發(fā)現(xiàn)則將value賦值給實例變量畏陕。
3.如果沒有發(fā)現(xiàn)setter或?qū)嵗兞颗渑遥瑒t調(diào)用setValue:forUndefinedKey:方法,并默認提出一個異常惠毁,但是一個NSObject的子類可以提出合適的行為犹芹。

上面提到了一個類屬性accessInstanceVariablesDirectly

 + (BOOL)accessInstanceVariablesDirectly

它表示是否允許讀取實例變量的值,如果為YES則在KVC查找的過程中鞠绰,從內(nèi)存中讀取屬性腰埂、實例變量的值。如果不允許外界通過KVC對我們的私有屬性和成員變量進行操作蜈膨,則可以設置此值為NO屿笼。
set的規(guī)則相對比較簡單牺荠,相信大家都能看懂。我們也可以按照這個搜索順序自己驗證是否符合驴一。(實現(xiàn)兩個setter方法以及添加四種成員變量休雌,然后依次注釋掉后檢查是不是按照上面的順序進行賦值操作。)

  • get的搜索規(guī)則
    get的搜索規(guī)則相對于set就有點復雜了

1.通過getter方法搜索實例肝断,例如get<Key>, <key>, is<Key>, _<key>的拼接方案杈曲。按照這個順序,如果發(fā)現(xiàn)符合的方法胸懈,就調(diào)用對應的方法并拿著結(jié)果跳轉(zhuǎn)到第五步担扑。否則,就繼續(xù)到下一步箫荡。
2.如果沒有找到簡單的getter方法魁亦,則搜索其匹配模式的方法countOf<Key>、objectIn<Key>AtIndex:羔挡、<key>AtIndexes:洁奈。如果找到其中的第一個和其他兩個中的一個,則創(chuàng)建一個集合代理對象NSKeyValueArray绞灼,該對象響應所有NSArray的方法并返回該對象利术。否則,繼續(xù)到第三步低矮。代理對象隨后將NSArray接收到的countOf<Key>印叁、objectIn<Key>AtIndex:、<key>AtIndexes:的消息給符合KVC規(guī)則的調(diào)用方军掂。當代理對象和KVC調(diào)用方通過上面方法一起工作時轮蜕,就會允許其行為類似于NSArray一樣。
3.如果沒有找到NSArray簡單存取方法蝗锥,或者NSArray存取方法組跃洛。則查找有沒有countOf<Key>、enumeratorOf<Key>终议、memberOf<Key>:命名的方法汇竭。如果找到三個方法,則創(chuàng)建一個集合代理對象穴张,該對象響應所有NSSet方法并返回细燎。否則,繼續(xù)執(zhí)行第四步皂甘。此代理對象隨后轉(zhuǎn)換countOf<Key>玻驻、enumeratorOf<Key>、memberOf<Key>:方法調(diào)用到創(chuàng)建它的對象上偿枕。實際上击狮,這個代理對象和NSSet一起工作佛析,使得其表象上看起來是NSSet。
4.如果沒有發(fā)現(xiàn)簡單getter方法彪蓬,或集合存取方法組,以及接收類方法accessInstanceVariablesDirectly是返回YES的捺萌。搜索一個名為_<key>档冬、_is<Key>、<key>桃纯、is<Key>的實例酷誓,根據(jù)他們的順序。如果發(fā)現(xiàn)對應的實例态坦,則立刻獲得實例可用的值并跳轉(zhuǎn)到第五步盐数,否則,跳轉(zhuǎn)到第六步伞梯。
5.如果取回的是一個對象指針玫氢,則直接返回這個結(jié)果。如果取回的是一個基礎數(shù)據(jù)類型谜诫,但是這個基礎數(shù)據(jù)類型是被NSNumber支持的漾峡,則存儲為NSNumber并返回。如果取回的是一個不支持NSNumber的基礎數(shù)據(jù)類型喻旷,則通過NSValue進行存儲并返回生逸。
6.如果所有情況都失敗,則調(diào)用valueForUndefinedKey:方法并拋出異常且预,這是默認行為槽袄。但是子類可以重寫此方法。

其中第二步搜索的意思是:沒有找到第一步中的簡單getter方法锋谐,但是實現(xiàn)了countOf<Key>以及objectIn<Key>AtIndex:遍尺、<key>AtIndexes:兩個中的其中一個,此時意味著當前對象擁有一個屬性名為<key>的NSKeyValueArray類型的屬性怀估,它可以響應NSArray的所有方法狮鸭。到這里其實就可以回答上面提到的第二個問題,一個對象不一定需要顯式的寫出自己的屬性也可以進行存取操作多搀!
第三步搜索的意思和第二步相似歧蕉,只是條件更苛刻,且最終返回的是NSSet對象康铭,響應NSSet的所有方法惯退。

了解了上面的set、get的搜索規(guī)則从藤,上面的第一個問題也就回答了催跪,蘋果底層會根據(jù)你傳入的key字符串按照搜索規(guī)則進行搜索锁蠕,并進行存取操作。

  • NSMutableArray 懊蒸、NSMutableSet和NSMutableOrderedSet對應的搜索規(guī)則

這幾種可變集合的搜索規(guī)則基本一致荣倾,只是搜索時調(diào)用的方法不同。詳細的搜索方法都可以在KVC官方文檔中找到骑丸。

以NSMutableArray為例說明:

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

搜索規(guī)則如下:

1.搜索insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AtIndexes , remove<Key>AtIndexes 格式的方法舌仍。如果至少找到一個insert方法和一個remove方法,那么同樣返回一個可以響應NSMutableArray所有方法代理集合(類名是NSKeyValueFastMutableArray2)通危,那么給這個代理集合發(fā)送NSMutableArray的方法铸豁,以insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AtIndexes , remove<Key>AtIndexes組合的形式調(diào)用。還有兩個可選實現(xiàn)的接口:replaceObjectAtIndex:withObject:,replace<Key>AtIndexes:with<Key>:菊碟。
2.如果上面的方法沒有找到节芥,則搜索set<Key>: 格式的方法,如果找到逆害,那么發(fā)送給代理集合的NSMutableArray最終都會調(diào)用set<Key>:方法头镊。 也就是說,mutableArrayValueForKey:取出的代理集合修改后忍燥,用set<Key>: 重新賦值回去去拧晕。這樣做效率會低很多。所以推薦實現(xiàn)上面的方法梅垄。
3.如果上一步的方法還還沒有找到厂捞,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),會按_<key>,<key>,的順序搜索成員變量名队丝,如果找到靡馁,那么發(fā)送的NSMutableArray消息方法直接交給這個成員變量處理。
4.如果還是找不到机久,則調(diào)用valueForUndefinedKey:臭墨。


上面調(diào)用的方法- (NSMutableArray )mutableArrayValueForKey:(NSString )key;其實有一個很重要的使用場景:KVO監(jiān)聽可變集合的變化**,當然這里的可變集合包括NSMutableArray膘盖,NSMutableSet侠畔,NSMutableOrderedSet软棺,但不包括NSDictionary茵宪。
通常使用KVO監(jiān)聽某個對象的可變集合屬性,當可變集合發(fā)生如add暖哨、remove等元素操作時憾股,對象并不能收到通知,具體原因可以留到下次分享KVO原理中說明。但是如果使用上面的這種方法獲取的可變集合,當內(nèi)部元素發(fā)生變化時也可以收到通知粉渠。


以上就是KVC的內(nèi)部原理,依據(jù)這樣的原理,我們下面嘗試自己實現(xiàn)系統(tǒng)的KVC機制

三扰路、自己實現(xiàn)

原理:給NSObject添加分類尤溜,實現(xiàn)自己的set、get方法,在方法中根據(jù)蘋果定義的搜索規(guī)則進行實現(xiàn)。
這里就以最簡單的set和get方法來說明自己實現(xiàn)的思路,權當拋磚引玉
為NSObject添加自己的KVC分類NSObject (NewKVC)

NSObject+NewKVC.h

@interface NSObject (NewKVC)
-(void)setMyValue:(id)value forKey:(NSString*)key;
-(id)myValueforKey:(NSString*)key;
@end
NSObject+NewKVC.m

@implementation NSObject (NewKVC)
- (void)setMyValue:(id)value forKey:(NSString *)key{
    if (key == nil || key.length == 0) {  //驗證key
        return;
    }
    if ([value isKindOfClass:[NSNull class]]) {
        [self setNilValueForKey:key]; //完全自定義需要自定義setMyNilValueForKey
        return;
    }
    if (![value isKindOfClass:[NSObject class]]) {
        @throw @"must be s NSObject type";
        return;
    }
  
    NSString* funcName = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(funcName)]) {  //默認優(yōu)先調(diào)用set方法
        [self performSelector:NSSelectorFromString(funcName) withObject:value];
        return;
    }
    unsigned int count;
    BOOL flag = false;
    Ivar* vars = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i<count; i++) {
        Ivar var = vars[i];
        NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
        
        if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
            flag = true;
            object_setIvar(self, var, value);
            break;
        }
        
        if ([keyName isEqualToString:key]) {
            flag = true;
            object_setIvar(self, var, value);
            break;
        }
    }
    if (!flag) {
        [self setValue:value forUndefinedKey:key];//如果完全自定義榜旦,那么需要寫一個setMyValue: forUndefinedKey:方法咐旧,這里必要性不是很大,就省略了
    }
}

- (id)myValueforKey:(NSString *)key{
    if (key == nil || key.length == 0) {
        return [NSNull new];
    }
    //這里沒有做相關集合的方法查詢
    NSString* funcName = [NSString stringWithFormat:@"get%@",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
        return [self performSelector:NSSelectorFromString(funcName)];
    }
    
    unsigned int count;
    BOOL flag = false;
    Ivar* vars = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i<count; i++) {
        Ivar var = vars[i];
        NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
        if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
            flag = true;
            return     object_getIvar(self, var);
            break;
        }
        if ([keyName isEqualToString:key]) {
            flag = true;
            return     object_getIvar(self, var);
            break;
        }
    }
    if (!flag) {
        [self valueForUndefinedKey:key];//需要自己實現(xiàn)myValueForUndefinedKey
    }
    return [NSNull new];
}
@end

注意.m中需要引入#import <objc/runtime.h>姚淆,因為我們需要動態(tài)獲取當前對象的成員變量搏讶,以便存取操作繁成。上面簡化版的KVC吓笙,只考慮了最簡單的情況,如果大家感興趣巾腕,完全可以實現(xiàn)一整套自己的KVC哦面睛。

四、使用場景

講了這么多原理尊搬,這么寫實現(xiàn)叁鉴,那KVC到底能用來干啥呢?如果你還不了解KVC的使用佛寿,那你就OUT啦
1.動態(tài)地設值和取值
這個應用就不多說了幌墓,最基本的應用
2.用KVC來訪問和修改私有變量
對于KVC來說但壮,一個對象沒有自己的隱私,只要它愿意常侣,就可以修改任何私有的東西蜡饵。不信可以試試在.m文件中聲明私有屬性或者成員變量,KVC一樣可以獲取到胳施。
3.多值操作(model和字典互轉(zhuǎn))

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

主要通過這兩個API來實現(xiàn)溯祸,也很簡單,不多介紹舞肆。
4.修改一些系統(tǒng)控件的內(nèi)部屬性
使用runtime來獲取Apple不想開放的成員變量焦辅,利用KVC進行修改。比如自定義tabbar椿胯,textfield等筷登,這個的應用也是比較常見。
5.用KVC實現(xiàn)高階消息傳遞
這個應用場景就比較少了哩盲,它的意思是在對容器類使用KVC時仆抵,valueForKey:將會被傳遞給容器中的每一個對象,而不是對容器本身進行操作种冬。比如下面的代碼:

    NSArray* arrStr = @[@"english",@"franch",@"chinese"];
    NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
    for (NSString* str  in arrCapStr) {
        NSLog(@"%@",str);
    }
    NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
    for (NSNumber* length  in arrCapStrLength) {
        NSLog(@"%ld",(long)length.integerValue);
    }

打印結(jié)果如下:

在這里插入圖片描述

6.用KVC中的函數(shù)來操作集合(集合主要指NSArray和NSSet,不包括NSDictionary)
集合運算符格式

上面的圖是集合運算符的格式舔糖,主要是對象調(diào)用valueForKeyPath:方法進行操作娱两。運算符有三種:
1)簡單集合運算符共有@avg, @count 金吗, @max 十兢, @min ,@sum5種

    NSArray* arrBooks = @[book1,book2,book3,book4];
    NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];

如上摇庙,arrBooks種存放的是4個Book對象旱物,[arrBooks valueForKeyPath:@"@sum.price"]的意思就是計算arrBooks中的每個Book對象的price的和。當然還會有:

    NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.price"];
    NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
    NSNumber* min = [arrBooks valueForKeyPath:@"@min.price"];
    NSNumber* max = [arrBooks valueForKeyPath:@"@max.price"];

2)對象運算符

@distinctUnionOfObjects
@unionOfObjects

它們的返回值都是NSArray卫袒,區(qū)別是前者返回的元素都是唯一的宵呛,是去重以后的結(jié)果;后者返回的元素是全集夕凝。
注意:以上兩個方法中宝穗,如果操作的屬性為nil,在添加到數(shù)組中時會導致Crash码秉。
比如:[arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
3)Array和Set嵌套操作符

@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets

這種操作是指數(shù)組嵌套數(shù)組或者Set嵌套Set等的操作逮矛,比如:

     NSArray *bookArrs = @[arrBooks,arrBooks];
    [bookArrs valueForKeyPath:@"@distinctUnionOfArrays.price"];

這樣就可以取出來多個Book數(shù)組中price不同的對象,是不是很贊转砖?

以上就是我總結(jié)出來的一點內(nèi)容须鼎,希望對你有幫助!

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市晋控,隨后出現(xiàn)的幾起案子汞窗,更是在濱河造成了極大的恐慌,老刑警劉巖糖荒,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杉辙,死亡現(xiàn)場離奇詭異,居然都是意外死亡捶朵,警方通過查閱死者的電腦和手機蜘矢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來综看,“玉大人品腹,你說我怎么就攤上這事『毂” “怎么了舞吭?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長析珊。 經(jīng)常有香客問我羡鸥,道長,這世上最難降的妖魔是什么忠寻? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任惧浴,我火速辦了婚禮,結(jié)果婚禮上奕剃,老公的妹妹穿的比我還像新娘衷旅。我一直安慰自己,他們只是感情好纵朋,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布柿顶。 她就那樣靜靜地躺著,像睡著了一般操软。 火紅的嫁衣襯著肌膚如雪嘁锯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天寺鸥,我揣著相機與錄音猪钮,去河邊找鬼。 笑死胆建,一個胖子當著我的面吹牛烤低,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播笆载,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼扑馁,長吁一口氣:“原來是場噩夢啊……” “哼涯呻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起腻要,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤复罐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后雄家,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體效诅,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年趟济,在試婚紗的時候發(fā)現(xiàn)自己被綠了乱投。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡顷编,死狀恐怖戚炫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情媳纬,我是刑警寧澤双肤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站钮惠,受9級特大地震影響茅糜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜素挽,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一限匣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧毁菱,春花似錦、人聲如沸锌历。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽究西。三九已至窗慎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卤材,已是汗流浹背遮斥。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扇丛,地道東北人术吗。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像帆精,于是被迫代替她去往敵國和親较屿。 傳聞我的和親對象是個殘疾皇子隧魄,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345