iOSKVC的实现原理

KVC

KVC: 全称Key-Value Coding,也称为键值编码。
KVC可以通过一个key间接访问某个对象属性。
KVC有两个特性:

  1. 可以访问私有成员变量;
  2. 可以修改私有或者系统的成员属性;

KVC的定义都是对NSObject的扩展来实现的(Objective-C中有个显式的NSKeyValueCoding类别名)。所以对于所有继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC

在这里插入图片描述

下面是KVC最为重要的四个方法:

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

其中,前两个是设置值方法,后面两个是取值方法。
KeyPath可以使用嵌套属性里面的属性值。Key不可以。

我们简单看一下KVC的使用:

YZPerson *person1 = [[YZPerson alloc] init];
person1.age = 10;//直接赋值
[person1 setValue:@"jack" forKey:@"name"];//间接赋值
[person1 setValue:@"120" forKeyPath:@"weight"];//间接赋值
[person1 setValue:@"Red" forKeyPath:@"car.color"];//属性里面的属性

NSLog(@"person1.age = %@, person1.name = %@, person1.weight = %d", [person1 valueForKey:@"age"], [person1 valueForKeyPath:@"name"], person1.weight);

运行结果:

2020-02-28 11:51:53.654921+0800 Category[1399:80227] person1.age = 10, person1.name = jack, person1.weight = 120
问:KVC修改属性是否可以触发KVO?

找个例子我们试一下:

@interface ViewController ()
@property (strong, nonatomic) Persion *p1;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Persion *p1 = [[Persion alloc] init];
    self.p1 = p1;
    [self.p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"context"];
    [self.p1 setValue:@"rose" forKey:@"name"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"%@ %@ %@", object, change, context);
}

- (void)dealloc
{
    [self.p1 removeObserver:self forKeyPath:@"name"];
}

@end

运行结果:

2020-02-28 13:48:56.604542+0800 test001[1432:397700] <Persion: 0x283918fa0> {
    kind = 1;
    new = rose;
    old = "<null>";
} context

从结果可以看出,使用KVC修改属性值(name),可以触发KVO的监听。

且,经过验证,[self.p1 setValue:@"rose" forKey:@"name"];这句代码进入了
- (void)setName:(NSString *)name方法里面。
也就是KVC的改变属性值,进入了属性的setter方法里面,
从而在didChangeValueForKey:方法中发送通知,实现KVO的监听。

并且
即使不写- (void)setName:(NSString *)name方法
或者
不写@property (strong, nonatomic) NSString *name;
即,使用成员变量name,而不是属性name

设置KVC的属性值改变,也可以使用KVO监听到属性值的改变,即触发KVO

因此,可以说,KVC是基于KVO的实现,KVC修改属性是可以触发KVO的。


KVC的工作流程

setValue:forKey:大致是这样工作的:
在这里插入图片描述
主要是:
先找方法
方法找到了,直接执行;
方法没有找到,则

判断是否可以直接访问属性
如果不可以直接访问属性,则Crash
如果可以执行访问属性,则

判断有没有响应的属性值
如果找到了属性,则直接执行;
如果没有找到属性值,则Crash

valueForKey:大致是这样工作的:
在这里插入图片描述
更多学习关于KVC:
iOS开发技巧系列—详解KVC(我告诉你KVC的一切)


KVC修改:字典、数组、对象属性、结构体

结构体的话,前面应该加上&,将地址传进去


KVC的Crash相关

问:哪些可能会造成KVC的Crash?

设置阶段的Crash

  • 设置值的时候,Key找不到,Key为nil,value为nil,value类型不匹配就会Crash

以下几种方法,都会造成KVC的Crash:

  1. key 不是对象的属性值,造成崩溃[self.person setValue:@"10" forKey:@"age2"];
  2. keyPath 不正确,造成崩溃[self.person setValue:@"10" forKeyPath:@"age.xxx"];
  3. key 为 nil,造成崩溃[self.person setValue:@"10" forKey:nil];
  4. value 为 nil,造成崩溃[self.person setValue:nil forKey:@"age"];
  5. value类型不对[self.person setValue:[[NSObject alloc] init] forKey:@"age"];
如何避免崩溃?

在这里插入图片描述
根据KVC设置时的查找过程,我们发现,当setValue:forKey: 执行失败会调用 setValue: forUndefinedKey: 方法,并引发崩溃。

那么,我们可以通过重写setValue: forUndefinedKey: 来避免上面第1、2种类型错误

1. key 不是对象的属性值
2. keyPath 不正确

造成的Crash

在这里插入图片描述

3. key 为nil

造成的Crash

我们可以利用 Method Swizzling 方法,在 NSObject 的分类中将 setValue:forKey: ysc_setValue:forKey: 进行方法交换。然后在自定义的方法中,添加对 key 为 nil 这种类型的判断。

4. value 为 nil

为非对象设值,造成崩溃 的情况

在调用 setValue:forKey: 方法时,系统如果查找到名为 set<Key>: 方法的时候,会去检测 value 的参数类型,如果参数类型为 NSNmber 的标量类型或者是 NSValue 的结构类型,但是 value 为 nil 时,会自动调用 setNilValueForKey: 方法。
这个方法的默认实现会引发崩溃。

所以为了防止这种情况导致的崩溃,我们可以通过重写 setNilValueForKey: 来解决。

- (void)setNilValueForKey:(NSString *)key
{
    NSLog(@"不能将%@设成nil",key);
}

取值阶段的Crash

  • 取值的时候,Key找不到
    当取值的时候,如果找不到key或者key为nil,也会Crash
  1. key 不是对象的属性值,造成崩溃NSLog(@"%@", [self.person valueForKey:@"age2"]);
  2. keyPath 不正确,造成崩溃NSLog(@"%@", [self.person valueForKeyPath:@"age.xxx"]);
  3. key 为 nil,造成崩溃NSLog(@"%@", [self.person valueForKey:nil]);
  4. value 为 nil,NSLog(@"%@", [nil valueForKey:@"age"]);,该方法编译器不通过

在这里插入图片描述

取值阶段的Crash可通过:
1和2可以通过重写valueForUndefinedKey: 方法
3通过方法交换,重写valueForUndefinedKey: ,在方法里面加非空判断
4重写setValue:forKey: 方法
避免Crash

iOS 开发:『Crash 防护系统』(三)KVC 防护


面试题

  • KVC能否改变对象的值

跟KVO监听对象一样,又是一个类似的题,经过试验发现,可以。

#import "YZPerson.h"

@interface ViewController ()
@property (strong, nonatomic) YZPerson *person;
@end

@implementation ViewController

//MRC下可以调用这个
//- (void)setPerson:(YZPerson *)person
//{
//    if (_person != person) {
//        [_person release];
//        _person = [person retain];
//    }
//}


- (void)viewDidLoad {
    [super viewDidLoad];
    
    YZPerson *person = [[YZPerson alloc] init];
    self.person = person;
    person.name = @"person";
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSObject *obj = [[NSObject alloc] init];
    [self setValue:obj forKeyPath:@"person"];//间接赋值

    NSLog(@"%@", self.person);
}

打印结果:
<NSObject: 0x6000028ae940>

可以看出,通过KVC可以将对象进行改变。


被问到的面试题:

下面两个方法的区别:

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;

setValue:forkey是KVC里面的方法
可以修改:
字典、数组、对象属性、结构体

setObject:forKey是NSMutableDictionary里面的方法,是为字典赋值

那么,在都是对NSMutableDictionary进行修改的情况下,

    NSMutableDictionary * dic = [@{@"name":@"小明"} mutableCopy];
    //都可以进行非空赋值
    [dic setValue:@"Men" forKey:@"gender"];
    [dic setObject:@"Men2" forKey:@"gender"];
    
    //setValue可以赋值nil,相当于删除key = gender的键值对
    [dic setValue:nil forKey:@"gender"];
    //setObject赋值nil,会崩溃
    //[dic setObject:nil forKey:@"gender"];
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值