KVC底層原理分析

一柱衔、前言

提起 KVC逊躁,大多數(shù)的第一反應是 setValue: forKey: 以及 setValue: forKeyPath:谢翎,這也就是我們的所說的鍵值編碼(Key-value coding)屹徘,鍵值編碼是一種由 NSKeyValueCoding 非正式協(xié)議啟用的機制走趋,對象采用該協(xié)議來提供對其屬性的間接訪問。當對象符合鍵值編碼時噪伊,可以通過簡潔簿煌、統(tǒng)一的消息傳遞接口通過字符串參數(shù)對其屬性進行尋址。詳細解釋可以進入官方文檔查閱鉴吹。接下來就一起跟我進入 KVC 的底層原理探索吧姨伟。

二、KVC 初探

1.KVC 的幾種使用方式

創(chuàng)建一個 Person 類豆励,在類中添加一些屬性夺荒。

Person.h
typedef struct {
    float x, y, z;
} ThreeFloats;
@interface LGPerson : NSObject
@property (nonatomic, copy)   NSString          *name;
@property (nonatomic, strong) NSArray           *array;
@property (nonatomic, strong) NSMutableArray    *mArray;
@property (nonatomic, assign) int age;
@property (nonatomic)         ThreeFloats       threeFloats;
@property (nonatomic, strong) Student           *student;
@end

1.1基本類型使用

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

// 給person對象 name 屬性賦值和取值
[person setValue:@"流年匆匆" forKey:@"name"];
[person valueForKey:@"name"];

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

// 嵌套屬性訪問
Student *student = [[Student alloc] init];
student.name    = @"xx";
person.student     = student;
[person setValue:@"學生" forKeyPath:@"student.name"];
NSLog(@"%@",[person valueForKeyPath:@"student.name"]);

1.2集合類型使用

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

// 修改不可變數(shù)組array的第一個值,
person.array = @[@"1",@"2",@"3"];
// 方法一瞒渠,修改為1
NSArray *array = @[@"1",@"2",@"3"];
[person setValue:array forKey:@"array"];
// 方法二,kvc的方法技扼,修改為10
NSMutableArray *arrayM = [person mutableArrayValueForKey:@"array"];
arrayM[0] = @"10";

1.3集合類型使用

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

// 字典轉(zhuǎn)模型
NSDictionary* dict = @{@"name":@"流年匆匆",@"age":@18};
[person setValuesForKeysWithDictionary:dict];

1.4集合類型使用

ThreeFloats floats = {1., 2., 3.};
// 非對象類型伍玖,需轉(zhuǎn)換成相應的NSValue
NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslutValue = [person valueForKey:@"threeFloats"];
NSLog(@"value = %@",reslutValue);

// 創(chuàng)建一個同類型結構體用來接收reslutValue
ThreeFloats th;
[reslutValue getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);

2. setValue:forKey: 底層原理探索

當我們調(diào)用 setValue:forKey: 的時候是怎么樣將值賦值到我們的對象里去的呢?


image.png

根據(jù)上面官方文檔得知:

1.第一步會先去對象里面查找是否有 set<Key>剿吻、_set<Key>窍箍、setIs<Key> 的訪問器(即方法)。
2.如果沒有找到訪問器并且類方法 accessInstanceVariablesDirectly 返回 YES丽旅,則會按順序去查找名稱為 _<key>椰棘、_is< key>、<key> 或 <key> 的實例變量榄笙,如果找到直接設置變量并完成晰搀。
3.如果方法和實例變量都沒找到,則會調(diào)用 setValue:forUndefinedKey: 方法办斑。
<br>說明: 這里的 "key" 指成員變量名字, 書寫格式需要符合 KVC 的命名規(guī)則外恕。

2.1 setKey: 方法驗證

創(chuàng)建 Person 類,并添加四個實例變量乡翅,以及添加 setName:鳞疲、_setName:、accessInstanceVariablesDirectly 方法蠕蚜。(下面驗證都是以這個對象為準尚洽,實例變量不變,方法變)

@interface Person : NSObject{
    @public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}

@implementation Person
//MARK: - setKey 的流程分析
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
@end

Person *person = [[Person alloc] init];
[person setValue:@"流年匆匆" forKey:@"name"];

結果:依次訪問順序

-[Person setName:] - 流年匆匆
-[Person _setName:] - 流年匆匆

如果將所有 set 方法注釋靶累,accessInstanceVariablesDirectly 返回 NO腺毫,則會報 '[<Person 0x6000033a5860> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 崩潰。

2.2 accessInstanceVariablesDirectly 返回 YES 后的實例變量驗證

@implementation Person
#pragma mark - 關閉或開啟實例變量賦值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}
@end

Person *person = [[Person alloc] init];
[person setValue:@"流年匆匆" forKey:@"name"];
NSLog(@"_name:%@-_isName:%@-name:%@-isName%@",person->_name,person->_isName,person->name,person->isName);
NSLog(@"_isName:%@-name:%@-isName:%@",person->_isName,person->name,person->isName);
NSLog(@"name:%@-isName%@",person->name,person->isName);
NSLog(@"isName:%@",person->isName);

將 accessInstanceVariablesDirectly 返回 YES挣柬,再將實例變量 _name潮酒、_isName、name邪蛔、isName 按順序注釋運行(NSLog也要依次注釋哦)急黎,得到的結果會是以下輸出。

1.KVC探索[4370:1716833] _name:流年匆匆-_isName:(null)-name:(null)-isName:(null)
2.KVC探索[4417:1720210] _isName:流年匆匆-name:(null)-isName:(null)
3.KVC探索[4445:1722057] name:流年匆匆-isName(null)
4.KVC探索[4468:1723450] isName:流年匆匆

3.valueForKey: 底層原理探索

1.Search the instance for the first accessor method found with a name like get<Key>, <key>, is<Key>, or _<key>, in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.
// 中間是集合類型的侧到,我們分析的是對象類型勃教,所以跳過,有興趣的可以自己看看
4.If no simple accessor method or group of collection access methods is found, and if the receiver’s class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.
5.If the retrieved property value is an object pointer, simply return the result.
If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that.
If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.
6.If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

根據(jù)官方文檔總體來說:

1.第一步會先去按順序查找 get<Key>, <key>, is<Key> 或 _<key> 的訪問器(即方法)匠抗。
2.如果沒有找到訪問器并且類方法 accessInstanceVariablesDirectly 返回 YES故源,則按順序搜索名為 _<key>, _is<key>汞贸,<key>绳军,或 is<Key> 的實例變量印机,如果找到,則直接獲取實例變量的值并將值轉(zhuǎn)換成相應類型返回删铃。
3.如果方法和實例變量都沒有找到,則調(diào)用 valueForUndefinedKey: 方法踏堡。

3.1 getKey: 方法驗證

@implementation Person
//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,
- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}
- (NSString *)name{
    return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}

Person *person = [[Person alloc] init];
NSLog(@"取值:%@",[person valueForKey:@"name"]);
@end

結果:依次訪問順序

1.KVC探索[4725:1958586] 取值:getName
2.KVC探索[4749:1978501] 取值:name
3.KVC探索[4749:1978501] 取值:isName
4.KVC探索[4749:1978501] 取值:_name

如果將所有 get 方法注釋猎唁,accessInstanceVariablesDirectly 返回 NO,則會報 '[<Person 0x600001ce28b0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 崩潰顷蟆。

3.2 accessInstanceVariablesDirectly 返回 YES 后實例變量驗證

#pragma mark - 關閉或開啟實例變量賦值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

Person *person = [[Person alloc] init];
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);

將 accessInstanceVariablesDirectly 返回 YES诫隅,再將實例變量 _name、_isName帐偎、name逐纬、isName 按順序注釋運行(賦值代碼也要依次注釋哦),得到的結果會是以下輸出削樊。

1.KVC探索[4792:2019099] 取值:_name
2.KVC探索[4792:2019099] 取值:_isName
3.KVC探索[4792:2019099] 取值:name
4.KVC探索[4792:2019099] 取值:isName

4. KVC 防崩潰處理

當我們在使用 setValue:forKey: 或者 valueForKey: 的時候豁生,由于 key 需要自己手寫且沒有提示,所以很可能會不小心寫錯漫贞,然后就會報 setValue:forUndefinedKey: 或者 valueForUndefinedKey: 的崩潰甸箱,如何防止這種崩潰呢?直接在當前類實現(xiàn) - (void)setValue:(id)value forUndefinedKey:(NSString *)key 和 - (id)valueForUndefinedKey:(NSString *)key 即可迅脐。

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"來了");
}
- (id)valueForUndefinedKey:(NSString *)key {
    return nil;
}
//MARK: 空置防崩潰
- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"設置 %@ 是空值",key);
}
//MARK: - 鍵值驗證 - 容錯 - 派發(fā) - 消息轉(zhuǎn)發(fā)
- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError{
    if([inKey isEqualToString:@"name"]){
        [self setValue:[NSString stringWithFormat:@"里面修改一下: %@",*ioValue] forKey:inKey];
        return YES;
    }
    *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的屬性",inKey,self] code:10088 userInfo:nil];
    return NO;
}

5. 拓展

除了 KVC 能給對象屬性賦值之外芍殖,其實我們經(jīng)常用的是點語法,例: person.name = @"流年匆匆";谴蔑,這種方法最終都會調(diào)用 reallySetProperty 函數(shù)對屬性進行賦值豌骏,而又根據(jù)屬性修飾符的不同,參數(shù)也是不一樣的隐锭,看下面源碼一目了然窃躲。

6. 總結

1.使用 setValue:forKey: 的時候,會先去順序查找對象是否有 set<Key>:钦睡、_set<Key>框舔、setIs<Key>:(雖然setIs<Key>:這個方法官方文檔上沒有寫,但確實是調(diào)用了的) 方法赎婚,有的話就調(diào)用進行賦值刘绣。
2.如果沒有找到,并且類方法 accessInstanceVariablesDirectly 返回 YES挣输,則會按順序去查找名稱為 _<key>纬凤、_is< key>、<key> 或 <key> 的實例變量撩嚼,如果找到直接設置變量并完成停士。
3.如果又沒找到方法和實例變量挖帘,則會調(diào)用 setValue:forUndefinedKey: 方法,如果對象沒有實現(xiàn) setValue:forUndefinedKey: 則會報 '[<Person 0x60000346a760> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 的崩潰恋技。
4.在使用 valueForKey: 的時候拇舀,先去按順序查找對象是否有 get<Key>, <key>, is<Key> 或 _<key> 的方法,有的話就返回蜻底。
5.如果沒有找到骄崩,并且類方法 accessInstanceVariablesDirectly 返回 YES,則會按順序搜索名為 _<key>薄辅, _is<key>要拂,<key>,或 is<Key> 的實例變量站楚,如果找到脱惰,則直接獲取實例變量的值并將值轉(zhuǎn)換成相應類型返回。
6.如果又沒找到方法和實例變量窿春,則調(diào)用 valueForUndefinedKey: 方法拉一。如果對象沒有實現(xiàn) valueForUndefinedKey: 則會報 '[<Person 0x600001ce28b0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key xxx.' 的崩潰。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末旧乞,一起剝皮案震驚了整個濱河市舅踪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌良蛮,老刑警劉巖抽碌,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異决瞳,居然都是意外死亡货徙,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門皮胡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痴颊,“玉大人,你說我怎么就攤上這事屡贺〈览猓” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵甩栈,是天一觀的道長泻仙。 經(jīng)常有香客問我,道長量没,這世上最難降的妖魔是什么玉转? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮殴蹄,結果婚禮上究抓,老公的妹妹穿的比我還像新娘猾担。我一直安慰自己,他們只是感情好刺下,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布绑嘹。 她就那樣靜靜地躺著,像睡著了一般橘茉。 火紅的嫁衣襯著肌膚如雪工腋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天捺癞,我揣著相機與錄音夷蚊,去河邊找鬼构挤。 笑死髓介,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的筋现。 我是一名探鬼主播唐础,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼矾飞!你這毒婦竟也來了一膨?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤洒沦,失蹤者是張志新(化名)和其女友劉穎豹绪,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體申眼,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡瞒津,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了括尸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片巷蚪。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖濒翻,靈堂內(nèi)的尸體忽然破棺而出屁柏,到底是詐尸還是另有隱情,我是刑警寧澤有送,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布淌喻,位于F島的核電站,受9級特大地震影響雀摘,放射性物質(zhì)發(fā)生泄漏似嗤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一届宠、第九天 我趴在偏房一處隱蔽的房頂上張望烁落。 院中可真熱鬧乘粒,春花似錦、人聲如沸伤塌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽每聪。三九已至旦棉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間药薯,已是汗流浹背绑洛。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留童本,地道東北人真屯。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像穷娱,于是被迫代替她去往敵國和親绑蔫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355