序
思考拓展题
为什么先进入objc_setProperty_atomic_copy
方法,而不是setName:
?
回答:
通用原则。即面向不同的数据类型,采用统一的属性设值的方法,会更加的高效,符合高内聚的原则。
一、概念及常见使用
1.0 概念
KVC(Key-Value coding) 是一个由
NSKeyValueCoding
协议触发的机制,允许对象通过该机制获取他们的属性。当一个对象符合KVC 原则,它的属性就可以用字符串通过一个简洁、相同的信息接口快捷访问到。这种简洁访问机制,借助实例变量量和关联存取方法补充了对象直接访问的机制。
1.1 简单使用
假设有类BankAccount
,有众多属性如下:
@interface BankAccount : NSObject |
给起通过KVC属性赋值,如下
[myAccount setValue:@(100.0) forKey:@"currentBalance"]; |
1.2 集合类型
对数组进行修改:
person.array = [@"1", @"2", @"3"]; |
- 方法一、创建新的数组(普通方法)
NSArray *array = [person valueForKey:@"array"]; |
- 方法二、通过可变数组赋值(KVC)
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"]; |
可见可变数组实行KVC 方法,更为简洁
1.3 集合操作符
集合操作符如下:
比如某个模型结构如下:
@interface Transaction : NSObject |
想要获取多个Transaction
的payee
属性的结合,可以用集合运算符
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"]; |
可见,集合运算符主要在于获取对象的值
1.4 访问非对象属性
问题:如何访问结构体?
这个问题经常出现在C、C++混编的环境下出现,解决的防范也比较简单,由于结构体并不遵循Key-Value Coding,可以先将其转为遵循KVC的NSValue
类型,然后通过KVC 对其进行存取。
给一个结构体ThreeFloats
,作为Person
的属性出现
typedef struct { |
转换为NSValue
ThreeFloats floats = {1., 2., 3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];存其值
[person setValue:value forKey:@"threeFloats"];
NSValue *reslut = [person valueForKey:@"threeFloats"];取其值
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);
打印结果如下:
1.5 通过keyPath 实现多级访问
这种情况出现在多级对象属性的情况下,如下面:
@interface Person : NSObject{ |
Student *student = [[Student alloc] init]; |
存值
通过keyPath 设置值的方式如下:
[person setValue:@"大师班" forKeyPath:@"student.subject"];
这里用到了
student.subject
实现多级别访问取值
也通过
student.subject
实现多级别访问NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
二、KVC 原理剖析
2.1 分析Setter 调用过程
对对象对实例变量赋值的流程如下:
- 查找setter 方法,如果存在,赋值成功
- 查看 accessInstanceVariablesDirectly (直接访问成员遍历)是否开启
- 若开启,查找 _
, _is , , or is - 若未开启,直接给变量赋值
- 若开启,查找 _
- 若以上都没执行,会报错,结果位
setValue:forUndefinedKey
2.2 分析Getter 取值过程
先查找Getter 方法,如果存在,取值成功。
采用的是标准的getter 方法
get<Key>, <key>, is<Key>, or _<key>
如果非集合类型:查看 accessInstanceVariablesDirectly (直接访问成员遍历)是否开启
- 如果开启——通过 _
, _is , , or is 查找实现取值 - 如果关闭——其他查找
- 如果开启——通过 _
如果没有,直接赋值
报错
valueForUndefinedKey
三、自定义KVC 方法
上面理解了KVC 的设值与取值过程,尝试自定义一个KVC。其实分析完上面的设值与取值,仿造的过程也是依样画葫芦。具体步骤如下:
3.1 Setter 方法
创建一个类的分类。在分类里创建取值和赋值的方法,有如下方法
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key;
判断key 的有效性
if(!key) || key.length == 0{ |
- 找到相应的的 set
、 _set 、setIs 方法,主要看本类是否相应对应名字的方法,实现如下:
NSString *Key = key.capitalizedString; |
看是否响应该方法
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{ |
判断是否能直接赋值实例变量
- 这是一个重要的开关.开启,则可以通过手动设值,跳到5
- 如果关闭,则由于本类不相应对应的setter 方法,无法有效赋值,会进行异常抛出如下:
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}找到相应的实例变量进行复制
定义一个收集实例变量的的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];获取相应的 ivar
if ([mArray containsObject:_key]) {
// 4.2 获取相应的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
}对相应的ivar 设置值
// 4.3 对相应的 ivar 设置值
object_setIvar(self , ivar, value);
return;
找不到相应的实例——报错处理
@throw [NSException exceptionWithName:@"UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
3.2 Getter 方法
getter 方法与 Setter 方法相仿,继续进行一下
设置自定义的getter 方法。代码如下:
- (id)lj_valueForKey: (NSString *)key;
判断key,保证不为空
if (key == nil || key.length == 0)
return nil;找到对应的get
、 、countOf 、objectIn AtIndex 等直接取值的方法。后两种一般是集合类型(数组等)用到的方法。 先对键
Key
进行大写转换// key 要大写
NSString *Key = key.capitalizedString;用get、countOf、objectIn 等方法,对key拼接
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];判断本类是否相应对应的get
、countOf 、objectIn AtIndex 方法,如果有,就执行该方法,直接获取到对应的值。
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}else if ([self respondsToSelector:NSSelectorFromString(key)]){
return [self performSelector:NSSelectorFromString(key)];
}else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
此时判断能否直接赋值实例变量。用到的是该类的
accessInstanceVariablesDirectly
属性是否开启。如果已打开,则表示可以通过取其 _
、_is 、 、is 来取值,跳到5 若未打开,抛出异常,表示找不到该键对应值
相关代码如下:
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
找到实例变量,进行取值
定义一个收集实例变量的可变数组:
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivars[i];
const char *ivarNameChar = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName == %@",ivarName);
[mArray addObject:ivarName];
}
free(ivars);依次判断该数组是否包含键关键字,其顺序为 _key——> _isKey——> key——> isKey,通过找到的关键字,进行取值
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
四、KVC 实用小技巧
4.1 自动转换类型
KVC 具备自动转换数据类型的功能
当我们对Int 类型的属性,添加字符串的类型,正常是这样的
@interface LGPerson : NSObject |
那么下面这种方式能成功吗?
[person setValue:@"20" forKey:@"age"]; |
可见是可以执行的,在通过KVC 设置之后,自动转换成了int
类型,整个数据成为了NSCFNumber。可见其灵活程度!
4.2 空值报警
总有些时候,设置值可能会异常为空,那怎么办呢?可以通过重写空值报警来避免异常
设值为空重写
- (void)setNilValueForKey:(NSString *)key{
NSLog(@"报告! 设置 %@ 是空值",key);
}取值为空重写
- (id)valueForUndefinedKey:(NSString *)key{
NSLog(@"报告!!!: %@ 没有这个key - 给你一个其他的吧,别奔溃了!",key);
return @"Master 牛逼";
}
4.3 键值验证
当类对应的键的值还是找不到,即将要报错了,我们还是可以自行挽救一下——先验证对应的键值是否成功,如果不成功,自行报错并写补救方法。
NSError *error; |
实现方法里,可以这样写
//MARK: - 键值验证 - 容错 - 派发 - 消息转发 |
五、总结
总的来说,KVC 是比较简单的一个章节,其原理是对类内部逐级查验,从set 方法、直接赋值开关、isKey 等键的取值实现赋值和取值的过程。而实现自定义KVC,则是依照逻辑一步一步写成。