(一)KVC (Key-Value Coding)
KVC,即键值编码,通过key来访问属性。
KVC依赖于Runtime,在Objective-C的动态性方法发挥了重要的作用。
它到底强在哪里呢?我们常听人说,“Objective-C没有真正的私有变量”,为什么会这么说呢?
- 通过Runtime可以直接获取所有的成员变量
- 通过KVC对成员变量直接访问读写
乍一看仿佛感觉平常用的并不多,其实在最开始还没有特别好的三方Model解析库出现时,我们都是通过-(void)setValue:(id)value forUndefinedKey:(NSString *)key
及-(void)setValuesForKeysWithDictionary:dic
来进行Json转对象的。
看下面的代码示例:
//.h
@interface ZQObject : NSObject
@property(nonatomic, copy)NSString *name;
@end
//.m
@implementation ZQObject
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"%@", key);
}
@end
//main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSDictionary *objDic = @{@"name":@"张三"};
ZQObject *obj = [[ZQObject alloc] initZQObject];
[obj setValuesForKeysWithDictionary:objDic];//line
NSLog(@"%@", obj);
}
return 0;
}
在上面的代码中,我们可以直接得知,obj对象的name属性已经复制成功。而.m文件中的方法意义则在于保证在没有定义的属性时不会crash,该方法后续再讲。
(1)常见的API
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
(2)访问属性常规用法
- 设置
-
setValue: forKey:
设置对象的属性值(单层) -
setValue: forKeyPath:
可以设置属性的属性(可以多层)
self.person = [[ZQPerson alloc]init];
//setValue: forKey:方法
[self.person setValue:@10 forKey:@"age"];
self.person.dog = [[ZQDog alloc] init];
//setValue: forKeyPath:方法
[self.person setValue:@20 forKeyPath:@"dog.height"];
- 访问(同理)
valueForKeyPath:
valueForKey:
//输出10 10
NSLog(@"%@ %@",[self.person valueForKey:@"age"],[self.person valueForKeyPath:@"dog.height"])
疑问:通过KVC设置成员变量的值,会触发KVO吗?
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[ZQPerson alloc]init];
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.person setValue:@20 forKey:@"age"];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"%@ %@ %@",keyPath,object,change);
}
-(void)dealloc{
[self.person removeObserver:self forKeyPath:@"age"];
}
输出结果:会触发!
age <ZQPerson: 0x60000026a5e0> {
kind = 1;
new = 20;
old = 0;
}
到底是什么原因呢?
(3)设值原理
①设置ZQPerson的_age成员变量为私有,添加两个方法(如下),重新运行
//ZQPerson.h
@interface ZQPerson : NSObject{
int _age;
}
-(void)setAge:(int)age;
-(void)_setAge:(int)age;
@end
//ZQPerson.m
@implementation ZQPerson
-(void)setAge:(int)age{
_age = age;
NSLog(@"setAge:%d",age);
}
-(void)_setAge:(int)age{
_age = age;
NSLog(@"其次找这里");
}
@end
输出结果:
setAge:20
age <ZQPerson: 0x600003304ac0> {
kind = 1;
new = 20;
old = 0;
}
②接着注释掉setAge
,再次执行:
_setAge:20
age <ZQPerson: 0x600002b442d0> {
kind = 1;
new = 20;
old = 0;
}
我们可以得出结论一:
setValue: forKey:
方法首先会查找setKey
,_setKey
的优先顺序先查找方法
④我们删除所有的set相关方法,重新运行:
age <ZQPerson: 0x6000015d5cc0> {
kind = 1;
new = 20;
old = 0;
}
KVO依然会触发!!!
⑤继续,我们重写+(BOOL)accessInstanceVariablesDirectly
方法,返回NO,发现报错了valueForUndefinedKey
reason: '[<ZQPerson 0x6000029ac7f0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key age.'
我们可以得出结论二:
如果没有找到方法,会判断该方法是否允许查找成员变量,不允许的情况下直接报错
⑥接下来,我们将+(BOOL)accessInstanceVariablesDirectly
返回YES,添加_age
,_isAge
,age
,isAge
四个成员变量:
@interface ZQPerson : NSObject{
int _age;
int _isAge;
int age;
int isAge;
}
@end
运行结果:
我们可以得出结论三:
若方法是否允许查找成员变量,则会按照
_age
,_isAge
,age
,isAge
的优先顺序给成员变量赋值,如果找不到,还是会报错valueForUndefinedKey
下面对上面的验证流程做一个总结:
只要通过
setValue: forKey:
的方法,只要赋值成功了,就会触发KVO(自己可以去代码验证)
可能在赋值过程内部,调用了willChangeValueForKey:
及didChangeValueForKey:
,可以通过重写这两个方法,发现确实会调用这两个方法(验证流程省略)
(4)取值原理
(5)KVC的进阶用法
上面我们讲述了KVC是什么,但是仅仅上面的介绍,还不足以证明KVC的强大之处
KVC除了上面可以直接给成员变量赋值之外,具体可以用来干什么呢?
1. keyPath访问
需要进行多步操作的时候我们可以通过keyPath一步到位 前面已举例,不再过多的阐述
2. 集合类型访问
我们在Person类中添加一个属性friends
@interface ZQObject : NSObject
@property(nonatomic, copy)NSString *name;
@property(nonatomic, strong)NSMutableArray *friends;
@end
我们可以发现在.m文件中有提示下面三个方法
-(NSUInteger)countOfFriends{
}
-(id)objectInFriendsAtIndex:(NSUInteger)index{
}
-(NSArray *)friendsAtIndexes:(NSIndexSet *)indexes{
}
这是KVC帮我们针对friends属性添加的一系列的方法中的几个,表示对friends属性支持KVC类型,方便我们使用KVC对其进行便携式访问。
如果体验到这个给我们带来的好处呢?
如果我们要给friends属性添加一个值
//常规写法
NSMutableArray *arr = [obj valueForKey:@"friends"];
[arr addObject:[[ZQObject alloc] initZQObject]];
//KVC对集合属性的便携式访问
[[obj mutableSetValueForKey:@"friends"] addObject:[[ZQObject alloc] initZQObject]];
如此这样做并不是仅仅为了减少一行代码,还可以对于不可变的集合类型提供安全的访问。以前的写法,如果friends为不可变数组,会直接crash.而后者会安全的访问,即使是不可变数组,也可以添加元素(重建一个新的不可变数组,并指向)
但是这样就违背了最初就想建立一个不可变数组的初衷,带来逻辑上的矛盾。所以这样不建议这样做,了解即可
3. KVC验证
前面提到过几次,使用KVC是有风险的,因为通过字符串去访问实例变量,虽然KVC提供复杂的查找逻辑来帮助对应到对应的成员变量,但仍然会发生找不到的情况,就会导致crash.
我们可以使用- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
KVC验证来验证值是否符合key所对应的类型或者值是否正确
下面我们看看常见的防止crash的方式:
//try-catch捕获异常
@try {
[obj setValue:@"张三" forKey:@"names"];
} @catch (NSException *exception) {
NSLog(@"%@", exception.userInfo);
} @finally {
NSLog(@"finish");
}
//在类.m中写上该方法 处理预期之外的属性匹配
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
//KVC验证
//main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
ZQObject *obj = [[ZQObject alloc] initZQObject];
ZQPerson *per = [[ZQPerson alloc] init];
NSError *error = nil;
BOOL isOK = [obj validateValue:&per forKey:@"names" error:&error];
if (isOK) {
[obj setValue:per forKey:@"name"];
}
}
return 0;
}
//ZQObject.m
//对不正确的类型设置默认值
-(BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
if ([* ioValue isKindOfClass:[NSString class]]) {
}else{
*ioValue = @"default name";
}
return YES;
}
//对不正确的类型设置判断为NO
-(BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
if ([* ioValue isKindOfClass:[NSString class]]) {
return YES;
}
return NO;
}
4. 数学运算
在NSKeyValueCoding.h中开头部分即定义了很多数学方法:
下面举一些常规的例子:
ZQObject *obj = [[ZQObject alloc] initWithName:@"张三" andAge:18];
ZQObject *obj2 = [[ZQObject alloc] initWithName:@"李四" andAge:20];
ZQObject *obj3 = [[ZQObject alloc] initWithName:@"王五" andAge:30];
NSArray *persons = @[obj, obj2, obj3];
NSNumber *sumAge = [persons valueForKeyPath:@"@sum.age"];
NSNumber *avgAge = [persons valueForKeyPath:@"@avg.age"];
NSNumber *maxAge = [persons valueForKeyPath:@"@max.age"];
NSNumber *minAge = [persons valueForKeyPath:@"@min.age"];
另外还有两种NSDistinctUnionOf...
和NSUnionOf...
共6个方法,分别对应Array、Dictionary、Set。
小结
- 由于KVC直接或间接访问成员变量,所以会存在不安全的情况
- KVC的验证相对麻烦些,需要对每一个验证属性写验证方法
- KVC的访问查找逻辑相对复杂,如果用它来JSON转Model,效率低下,尽量不用
- KVC依然为我们提供了一些便利,开发者应扬长避短,充分发挥KVC的强项