之前簡單得講過一些KVC的用法,但是並不能深入理解KVC
內部實現及其原理,下面主要講下KVC的底層原理。
取值 valueForKey:
在使用KVC
取值的時候,使用valueForKey:
方法,該方法會返回一個id
類型的對象,那麼它的內部會怎麼處理的呢?現在我們使用該方法:
Teacher *teacher = [[Teacher alloc] init];
NSString *name = [teacher valueForKey:@"name"];
NSLog(@"%@",name);
分析:當使用KVC獲取成員變量的值時,其內部會首先去找getter
方法- (NSString *)getName
,如果有該方法直接調用;如果沒有就去找getter
方法- (NSString *)name
,如果有直接調用;如果沒有就去找getter方法- (NSString *)getIsName
,如果有直接調用;如果沒有就去找getter方法:- (NSString *)isName
,如果有直接調用;如果沒有這裏就會出現轉折,程序不會再去找getter
方法,而是去找下面這兩個方法:
// 數組元素數
- (NSInteger)countOfName {
return 5;
}
// 數組內容
- (id)objectInNameAtIndex:(NSUInteger)index {
if (index == 0) {
return @"Michael";
}
return @"Tom";
}
如果找到了這個兩個方法,就會返回一個數組類型:
這裏的場景雖然還沒有遇到過,但是KVC
內部確實是在沒有訪問到getter
方法的時候,會訪問這個兩個方法,如果有直接返回;如果沒有就開始訪問成員變,在訪問成員變量之前會有個判斷方法:+ (BOOL)accessInstanceVariablesDirectly
,該方法默認是YES
,如果重寫該方法並返回NO
,那麼將失去訪問成員變量的權限,也就是說在沒找到getter
方法,也沒找到數組方法的時候程序就會異常:valueForUndefinedKey:
。下面討論在accessInstanceVariablesDirectly:
返回YES
的情況,開始去找成員變量,具體用法如下:
// Teacher.h
@interface Teacher : NSObject
{
NSString * _name;
NSString * _isName;
NSString * name;
NSString * isName;
}
@end
// Teacher.m
@implementation Teacher
- (instancetype)init {
if (self = [super init]) {
_name = @"_name";
_isName = @"_isName";
name = @"name";
isName = @"isName";
}
return self;
}
@end
上例中在Teacher類中定義了四個成員變量,然後使用KVC取出成員變量name的值,會打輸出什麼呢?輸出結果是:@"_name"
;如果將成員變量_name
及其初始化值註釋,輸出又會如何呢?輸出結果是:@"_isName"
…以此類推。結論很出人意料,雖然使用KVC獲取的是成員變量name的值,但是卻可以獲取到四種值,它首先會到對象的成員變量中尋找名爲“_key”
的成員變量,如果有就取出值;如果沒有就去找名爲“_isKey”
的成員變量,如果有就取出值;如果沒有就去找名爲“key”
的成員變量,如果有就取出值;如果沒有就去找名爲“isKey”
的成員變量,如果有就取出值;如果沒有程序會crash,報錯原因是:
valueForUndefinedKey:
reason: '[<Teacher 0x60000118fd50> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'
那麼如果我們不想讓程序crash,可以重寫valueForUndefinedKey:
方法:
- (id)valueForUndefinedKey:(NSString *)key {
return nil;
}
此時即使key
不存在,會返回null
,而不會異常。
設值 setValue: forKey:
前面說使用valueForKey:
方法獲取值,現在說使用setValue: forKey:
設值,那麼該方法底層會怎麼執行呢?首先回去找setter方法:
- (void)setName:(NSString *)name {
NSLog(@"set name");
}
如果找到沒找到setName:
,就會去找:
- (void)setIsName:(NSString *)name {
NSLog(@"set isName");
}
如果沒找setIsName:
方法,這裏不會再去找別的setter
方法,而是去查找成員變量,在查找之前還是需要看看accessInstanceVariablesDirectly:
的返回值,如果返回NO,則失去訪問成員變量的權限,程序creash;如果返回YES,就去查找成員變量,訪問成員變量的優先級和valueForKey:
方法一樣(_key > _isKey > key > isKey
),如果在查找成員變量時也沒有找到key,此時程序crash。
爲了避免crash,我們重寫setValue: forUndefinedKey:
方法:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"沒有找到這個%@",key);
}
注意:如果成員變量是基礎數據類型(int、float等),在使用setValue: forKey:
設值時,不能設值爲nil,不然程序crash,異常原因setNilValueForKey
:
reason: '[<Teacher 0x6000021b72a0> setNilValueForKey]: could not set nil as the value for the key age.'
這時我們可以重寫setNilValueForKey
方法,可以避免這種異常:
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"不能將 %@ 設置爲nil",key);
}
我們會注意到valueForKey:
方法返回值是id
類型的,如果我們成員變量是基礎數據類型而不是對象類型,這該如何處理呢?對於KVC,Cocoa會自動裝箱和開箱標量值,在取值的時候其實返回的是NSNumber
類型。
監聽數組 mutableArrayValueForKey:
當數組受到KVO監聽時,如果向數組裏面添加元素會被監聽到嗎?例如:
[_teacher addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"new value %@",change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 監聽數組的變化
[_teacher.array addObject:@"1"];
}
- (void)dealloc {
[_teacher removeObserver:self forKeyPath:@"array"];
}
當向數組裏面添加元素時,顯然沒有被監聽到,這時候需要用到KVC的mutableArrayValueForKey:
方法,該方法返回一個可變數組,然後再向裏面添加元素:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 監聽數組的變化
//[_teacher.array addObject:@"1"];
static int i = 0;
[[_teacher mutableArrayValueForKey:@"array"] addObject:@(i++)];
NSLog(@"%@",[_teacher mutableArrayValueForKey:@"array"]);
}
KVC的快速運算
先看一個例子:
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
Teacher *t = [[Teacher alloc] init];
t.age = i;
[array addObject:t];
}
利用KVC打印數組的個數:
NSLog(@"%@",[array valueForKey:@"@count"]);
使用@count
運算符來求出數組個數,還有一些常見操作符
@sum
:求和運算符;@min
:求最小值運算符;@max
:求最大值運算符;@avg
:求平均數運算符。
NSLog(@"%@",[array valueForKeyPath:@"@sum.age"]);//求和
NSLog(@"%@",[array valueForKeyPath:@"@min.age"]);//最小值
NSLog(@"%@",[array valueForKeyPath:@"@max.age"]);//最大值
NSLog(@"%@",[array valueForKeyPath:@"@avg.age"]);//平均值
//獲取到所有age的一個集合(非數組);特點:無序、不重複
NSLog(@"%@",[array valueForKeyPath:@"@distinctUnionOfObjects.age"]);
注意:這裏使用的是valueForKeyPath:
方法,表示鍵路徑,可以在對象和不同變量之間用圓點隔開,這些鍵路徑的深度是任意的,具體取決於對象的複雜度,常用於對象的符合和快速運算,例如一個Person類中複合一個Teacher類,在Teacher類中再複合一個Student類,這時利用KVC取Student類中的一個成員變量的值,可以表示爲:
NSString *name = [person valueForKeyPath:@"teacher.student.name"];
同樣的還有setValue: forKeyPath:
修改成員變量值的方法:
[person setValue:@"John" forKeyPath:@"teacher.student.name"];
簡單實現 setValue: forKey:
下面自己簡單的實現以下setValue: forKey:
和valueForKey:
方法,創建一個NSObject的分類:
-(void)YZ_setValue:(id)value forKey:(NSString *)key
{
if (key == nil || key.length == 0) {
return;
}
// 尋找setKey方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(setKey)]) {
[self performSelector:NSSelectorFromString(setKey) withObject:value];
return;
}
// 尋找setIsKey方法
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(setIsKey)]) {
[self performSelector:NSSelectorFromString(setIsKey) withObject:value];
return;
}
// setter方法都沒有找到去找成員變量
// 先判斷訪問成員變量權限
if ([[self class] accessInstanceVariablesDirectly]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
// _key
for (NSInteger i = 0; i< outCount; i++) {
Ivar ivar = ivars[i];
// 成員變量名稱
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
// 給成員變量賦值
object_setIvar(self, ivar, value);
return;
}
}
// _isKey
for (NSInteger i = 0; i< outCount; i++) {
Ivar ivar = ivars[i];
// 成員變量名稱
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_Is%@",key.capitalizedString]]) {
// 給成員變量賦值
object_setIvar(self, ivar, value);
return;
}
}
// key
for (NSInteger i = 0; i< outCount; i++) {
Ivar ivar = ivars[i];
// 成員變量名稱
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:key]) {
// 給成員變量賦值
object_setIvar(self, ivar, value);
return;
}
}
// isKey
for (NSInteger i = 0; i< outCount; i++) {
Ivar ivar = ivars[i];
// 成員變量名稱
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"is%@",key.capitalizedString]]) {
// 給成員變量賦值
object_setIvar(self, ivar, value);
return;
}
}
// 釋放(new copy create)
free(ivars);
} else {
// 不允許訪問成員變量,拋出異常
NSException *exception = [NSException exceptionWithName:@"YZKVC Exception" reason:@"accessInstanceVariablesDirectly method retun NO!" userInfo:nil];
@throw exception;
return;
}
}
- (id)YZ_valueForKey:(NSString *)key {
if (key == nil || key.length == 0) {
return nil;
}
// 先找getter方法
NSString *getKey = [NSString stringWithFormat:@"get%@",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}
if ([self respondsToSelector:NSSelectorFromString(key)]) {
return [self performSelector:NSSelectorFromString(key)];
}
NSString *getIsKey = [NSString stringWithFormat:@"getIs%@",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(getIsKey)]) {
return [self performSelector:NSSelectorFromString(getIsKey)];
}
// 沒有查找到getter方法
NSString *countName = [NSString stringWithFormat:@"countOf%@",key.capitalizedString];
NSString *objectInName = [NSString stringWithFormat:@"objectIn%@AtIndex:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(countName)] && [self respondsToSelector:NSSelectorFromString(objectInName)]) {
NSMutableArray *array = [NSMutableArray array];
NSInteger count = [self performSelector:NSSelectorFromString(countName)];
for (NSInteger i = 0; i < count; i++) {
// 這裏好像有點問題
id objc = [self performSelector:NSSelectorFromString(objectInName) withObject:@(i)];
NSLog(@"%@ %d",objc,i);
[array addObject:objc];
}
return [NSArray arrayWithArray:array];
}
// 去找成員變量
if ([[self class] accessInstanceVariablesDirectly]) {
// 優先級與YZ_setValue: forKey: 一致
unsigned int outCount = 0;
Ivar * ivars = class_copyIvarList([self class], &outCount);
// _key
for (NSInteger i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
return object_getIvar(self, ivar);
}
}
// _isKey
for (NSInteger i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_is%@",key.capitalizedString]]) {
return object_getIvar(self, ivar);
}
}
// key
for (NSInteger i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:key]) {
return object_getIvar(self, ivar);
}
}
// isKey
for (NSInteger i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
NSString *keyName = [NSString stringWithUTF8String:name];
if ([keyName isEqualToString:[NSString stringWithFormat:@"is%@",key.capitalizedString]]) {
return object_getIvar(self, ivar);
}
}
free(ivars);
} else {
NSException *exception = [NSException exceptionWithName:@"YZKVC Exception" reason:@"accessInstanceVariablesDirectly method retun NO!" userInfo:nil];
@throw exception;
return nil;
}
return nil;
}
調用:
Teacher *teacher = [[Teacher alloc] init];
[teacher YZ_setValue:@"John" forKey:@"name"];
NSLog(@"%@",[teacher YZ_valueForKey:@"name"]);