本文只是按照自己思路實(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):
- 筆者的實(shí)現(xiàn)比系統(tǒng)的慢 7~20 倍左右;
- 如同前言所述,筆者的實(shí)現(xiàn)功能弱胁富病室琢;
- 筆者的實(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ò)誤。