實(shí)現(xiàn)自己的 KVC

本文只是按照自己思路實(shí)現(xiàn)了 setValue:forKey:setValue:forKeyPath: 這兩個(gè)方法,所以這個(gè)標(biāo)題起得有點(diǎn)夸張了瘟芝,KVC 跟容器類的交互性宏、對(duì)標(biāo)量的封裝以及高階消息傳遞等特性踩验,我一個(gè)都沒(méi)實(shí)現(xiàn)。??我是來(lái)熟悉 runtime 的 API 來(lái)的赴恨。

KVC 實(shí)現(xiàn)的猜想

關(guān)于 Apple 是如何實(shí)現(xiàn) KVC 這個(gè)問(wèn)題疹娶,我表示一臉懵逼。因?yàn)闊o(wú)法打印出所有調(diào)用過(guò)的函數(shù)的名字伦连,所以我根本不指望能知道 KVC 背后的一切雨饺,只能打些可能會(huì)被調(diào)用的函數(shù)的符號(hào)斷點(diǎn)胡搞瞎搞。透過(guò)表象假裝看本質(zhì)惑淳,以下都是猜想:對(duì)于 setValue:forKey: 额港,在 setter 存在的情況,KVC 會(huì)直接發(fā)送 -set<Key>: 消息賦值歧焦;在 setter 不在的情況下移斩,會(huì)按文檔所闡述的規(guī)則(_<key>, _<isKey>, <key>, is<Key>)去查找實(shí)例變量,檢查其是否存在绢馍,之后通過(guò)與 object_setIvar 給它賦值向瓷。對(duì)于 setValue:forKeyPath:,遞歸地檢查 keyPath 中的屬性是否存在舰涌,到達(dá)路徑最深處事再調(diào)用 setValue:forKey: 完成賦值猖任。

動(dòng)手實(shí)現(xiàn)

謹(jǐn)防浪費(fèi)大家時(shí)間,往下看之前舵稠,請(qǐng)明白三點(diǎn):

  1. 筆者的實(shí)現(xiàn)比系統(tǒng)的慢 7~20 倍左右;
  2. 如同前言所述,筆者的實(shí)現(xiàn)功能弱胁富病室琢;
  3. 筆者的實(shí)現(xiàn)比系統(tǒng)的慢 7~20 倍左右。

給 NSObject 整個(gè) Category落追,實(shí)現(xiàn)這兩個(gè) API oz_setValue:forKey:oz_setValue:forKeyPath: 代碼里面寫了注釋盈滴,所以廢話不多說(shuō):

- (void)oz_setValue:(id)value forKey:(NSString *)key {
    
    // OZ_SetterNameForkey 的作用是構(gòu)造形如 `setKey:` 的字符串
    // 這里用 `object_getClass(self)` 而不用 `[self class]` 是為了裝逼,下同
    SEL setterSelector = NSSelectorFromString(OZ_SetterNameForkey(key));
    Method setterMethod = class_getInstanceMethod(object_getClass(self), setterSelector);

    // 如果這個(gè) setter 存在咧轿钠,那么向當(dāng)前對(duì)象發(fā)送 setter 消息
    if (setterMethod) {
        void (*objc_msgSendCasted)(id, SEL, ...) = (void *)objc_msgSend;
        return objc_msgSendCasted(self, setterSelector, value);
    }
    
    // 如果 setter 不存在巢钓,且不允許直接訪問(wèn)成員變量的話,就會(huì)被判斷為給 undefined key 賦值
    if (![[self class] accessInstanceVariablesDirectly]) {
        if ([self respondsToSelector:@selector(setValue:forUndefinedKey:)]) {
            return [self setValue:value forUndefinedKey:key];
        }
        @throw OZ_UnknowKeyException(self, key);
    }

    // 在當(dāng)前對(duì)象中所屬的類中找到符合 KVO 命名規(guī)則的實(shí)例變量
    Ivar target = OZ_Class_GetKVOCompliantIvar(object_getClass(self), key);
    if (!target) {
        @throw OZ_UnknowKeyException(self, key);
    }
    
    // 給當(dāng)前對(duì)象的實(shí)例變量賦值
    object_setIvar(self, target, value);
}

-(void)oz_setValue:(id)value forKeyPath:(NSString *)keyPath {
    if (!keyPath) {
        @throw OZ_UnknowKeyException(self, keyPath);
    }
    
    // 返回第一個(gè) '.' 的位置疗垛,如果大于或等于 keyPath 的長(zhǎng)度症汹,說(shuō)明不存在,交由 `oz_setValue:forKey:` 處理
    NSUInteger separetedDotIndex = OZ_IndexOfFirstDot(keyPath);
    if (separetedDotIndex >= keyPath.length) {
        return [self oz_setValue:value forKey:keyPath];
    }
    
    // 從第一個(gè) '.' 分割 keyPath
    NSString *firstKey = [keyPath substringToIndex:separetedDotIndex];
    NSString *restKeyPath = [keyPath substringFromIndex:separetedDotIndex + 1];

    Ivar currentIvar = OZ_Class_GetKVOCompliantIvar(object_getClass(self), firstKey);
    
    // 注意贷腕,這里要把對(duì)象所對(duì)于的實(shí)例變量的值取出來(lái)背镇,它將會(huì)是下一次發(fā)送消息的接收者
    id ivarValue = object_getIvar(self, currentIvar);
    
    // 如果剩余的 keyPath 沒(méi)有 '.',那么這個(gè) keyPath 就是最后要設(shè)置的變量泽裳,
    // ivarValue 擁有這個(gè)對(duì)象瞒斩,所以它是消息的接收者,交由 `oz_setValue:forKey:` 處理
    if (![restKeyPath containsString:@"."]) {
        void (*objc_msgSendCasted)(id, SEL, ...) = (void *)objc_msgSend;
        return objc_msgSendCasted(ivarValue, @selector(oz_setValue:forKey:), value, restKeyPath);
    }
    
    // 遞歸調(diào)用
    [ivarValue oz_setValue:value forKeyPath:restKeyPath];
}

貼完了兩段有臭又長(zhǎng)的代碼涮总,接下來(lái)是 OZ_Class_GetKVOCompliantIvar 的實(shí)現(xiàn):

static Ivar OZ_Class_GetKVOCompliantIvar(Class cls, NSString *key) {
    NSString *firstLetter = [[key substringToIndex:1] lowercaseString];
    NSString *Key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1)
                                                 withString:firstLetter];
    //(_key, _isKey, key, isKey)
    NSArray *kvoCompliantKeys = @[[NSString stringWithFormat:@"_%@", key],
                                  [NSString stringWithFormat:@"_is%@", Key],
                                  [NSString stringWithFormat:@"%@", key],
                                  [NSString stringWithFormat:@"is%@", Key],];
    Ivar target = NULL;
    for (NSString *kvoKey in kvoCompliantKeys) {
        target = class_getInstanceVariable(cls, [kvoKey UTF8String]);
        if (target) {
            break;
        }
    }
    return target;
}

除了構(gòu)建四個(gè)遵從 KVC 規(guī)則的實(shí)例變量名外胸囱,值得一提的是 class_getInstanceVariable 這個(gè)函數(shù),它在系統(tǒng)調(diào)用 setValue:forKeyPath: 時(shí)不會(huì)被調(diào)用到瀑梗,所以 setValue:forKeyPath: 并不是通過(guò)這個(gè)這個(gè)函數(shù)來(lái)檢驗(yàn)每一級(jí)屬性的存在烹笔。這個(gè)東西待優(yōu)化的空間還有很多,挖個(gè)坑遲些再折騰夺克,要繼續(xù)準(zhǔn)備校招的事情了箕宙。

最后

起初對(duì)于 runtime 的 API 的不熟悉,我并不知道 class_getInstanceVariable 的存在铺纽,然后想利用胡搞瞎搞發(fā)現(xiàn)的函數(shù) _class_getVariable 來(lái)獲得變量柬帕,但是在運(yùn)行時(shí)得到錯(cuò)誤,我在 stackoverflow 上提了個(gè)關(guān)于調(diào)用 dylib 中的函數(shù)的問(wèn)題狡门,如果知道怎么整希望能指點(diǎn)我一下陷寝,或者指出我的錯(cuò)誤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末其馏,一起剝皮案震驚了整個(gè)濱河市凤跑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌叛复,老刑警劉巖仔引,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扔仓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡咖耘,警方通過(guò)查閱死者的電腦和手機(jī)翘簇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)儿倒,“玉大人版保,你說(shuō)我怎么就攤上這事》蚍瘢” “怎么了彻犁?”我有些...
    開(kāi)封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)凰慈。 經(jīng)常有香客問(wèn)我汞幢,道長(zhǎng),這世上最難降的妖魔是什么溉瓶? 我笑而不...
    開(kāi)封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任急鳄,我火速辦了婚禮,結(jié)果婚禮上堰酿,老公的妹妹穿的比我還像新娘疾宏。我一直安慰自己,他們只是感情好触创,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布坎藐。 她就那樣靜靜地躺著,像睡著了一般哼绑。 火紅的嫁衣襯著肌膚如雪岩馍。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天抖韩,我揣著相機(jī)與錄音蛀恩,去河邊找鬼。 笑死茂浮,一個(gè)胖子當(dāng)著我的面吹牛双谆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播席揽,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼顽馋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了幌羞?” 一聲冷哼從身側(cè)響起寸谜,我...
    開(kāi)封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎属桦,沒(méi)想到半個(gè)月后熊痴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體他爸,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年果善,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了讲逛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡岭埠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蔚鸥,到底是詐尸還是另有隱情惜论,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布止喷,位于F島的核電站馆类,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏弹谁。R本人自食惡果不足惜乾巧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望预愤。 院中可真熱鬧沟于,春花似錦、人聲如沸植康。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)销睁。三九已至供璧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冻记,已是汗流浹背睡毒。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留冗栗,地道東北人演顾。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像贞瞒,于是被迫代替她去往敵國(guó)和親偶房。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

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

  • KVC(Key-value coding)鍵值編碼军浆,單看這個(gè)名字可能不太好理解棕洋。其實(shí)翻譯一下就很簡(jiǎn)單了,就是指iO...
    朽木自雕也閱讀 1,560評(píng)論 6 1
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉乒融,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,719評(píng)論 0 9
  • KVC(Key-value coding)鍵值編碼掰盘,單看這個(gè)名字可能不太好理解摄悯。其實(shí)翻譯一下就很簡(jiǎn)單了,就是指iO...
    黑暗中的孤影閱讀 49,740評(píng)論 74 441
  • KVC簡(jiǎn)單介紹 KVC(Key-value coding)鍵值編碼愧捕,就是指iOS的開(kāi)發(fā)中奢驯,可以允許開(kāi)發(fā)者通過(guò)Key...
    公子無(wú)禮閱讀 1,394評(píng)論 0 6
  • 2017已經(jīng)接近尾聲了,靜靜的閉上眼睛細(xì)細(xì)回想這一年次绘,這一年收獲不少瘪阁。常言道每天進(jìn)步一點(diǎn)點(diǎn),每一年也要比去年有進(jìn)步...
    優(yōu)樂(lè)維兒閱讀 189評(píng)論 2 1