一柱衔、前言
提起 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: 的時候是怎么樣將值賦值到我們的對象里去的呢?
根據(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.' 的崩潰。