KVC底层原理(设置取值原理)

KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问一个属性。

常见的API有
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key; 

我们先来了解一下KVC的基本使用。

@interface Cat : NSObject
@property (nonatomic, assign) int weight;
@end

@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, strong) Cat *cat;
@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    
    Cat *cat = [[Cat alloc] init];
    person.cat = cat;
    
    //设值
    //以下三句都可以为age设值
    person.age = 10;
    [person setValue:@11 forKey:@"age"];
    [person setValue:@12 forKeyPath:@"age"];
    //以下两句都可以为weight设值
    cat.weight = 5;
    [person setValue:@6 forKeyPath:@"cat.weight"];
    
    //取值
    NSLog(@"%d - %@ - %@",person.age,[person valueForKey:@"age"],[person valueForKeyPath:@"age"]);
    NSLog(@"%d - %@",person.cat.weight,[person valueForKeyPath:@"cat.weight"]);
}

@end

打印结果
2019-06-28 09:52:53.175189+0800 KVC底层原理(设值取值原理)[7779:29480939] 12 - 12 - 12
2019-06-28 09:52:53.175300+0800 KVC底层原理(设值取值原理)[7779:29480939] 6 - 6

由上面代码可以看出,setValue: forKey:、setValue: forKeyPath:都可以设值,valueForKey:、valueForKeyPath:都可以取值。keyPath结尾的两个方法,可以从路径上设置和取值,稍微更强大一点。

KVC设值原理

首先我们来看一张KVC设值流程原理图,setValue:forKey:内部调用流程

  • 1.按照setKey:,_setKey:顺序查找方法,找到了就调用方法传递参数。
  • 2.第一步没找到就会调用accessInstanceVariablesDirectly方法,该方法返回值为NO时直接调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException,方法返回值是YES的时候进入第三步。该方法默认值是返回YES。
  • 3.按照_key、_isKey、key、isKey顺序查找成员变量,找到了就直接赋值,没找到依然是调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException。
KVC设值原理图

接下来我们用代码验证一下以上流程

  1. 首先在Person类.h中不定义属性,只在.m文件实现setAge:和_setAge:方法,此时在ViewController类中创建Person的实例对象person,通过KVC给age赋值。

运行程序,会优先调用setAge:方法。

@implementation Person

- (void)setAge:(int)age{
    NSLog(@"setAge: - %d",age);
}

- (void)_setAge:(int)age{
    NSLog(@"_setAge: - %d",age);
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    
    [person setValue:@10 forKey:@"age"];
}

@end

打印结果
2019-06-28 11:20:56.582913+0800 KVC底层原理(设值取值原理)[8766:29719538] setAge: - 10

注释setAge:方法则会调用_setAge:方法

@implementation Person

//- (void)setAge:(int)age{
//    NSLog(@"setAge: - %d",age);
//}

- (void)_setAge:(int)age{
    NSLog(@"_setAge: - %d",age);
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    
    [person setValue:@10 forKey:@"age"];
}

@end

运行结果
2019-06-28 11:21:56.416325+0800 KVC底层原理(设值取值原理)[8793:29725184] _setAge: - 10
  1. 此时我们在Person.h中直接定义成员变量,因为定义属性,系统会自动生成set方法,拿样就验证不了下面的步骤。所以分别定义_age,_isAge,age,isAge四个成员变量,并且实现accessInstanceVariablesDirectly方法,分别研究在返回值为YES和NO的流程。

1>返回值为NO,直接崩溃报错NSUnknownKeyException

@interface Person : NSObject
{
    int _age;
    int _isAge;
    int age;
    int isAge;
}

@end

//- (void)setAge:(int)age{
//    NSLog(@"setAge: - %d",age);
//}

//- (void)_setAge:(int)age{
//    NSLog(@"_setAge: - %d",age);
//}

+ (BOOL)accessInstanceVariablesDirectly{
    return NO;
}

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    
    [person setValue:@10 forKey:@"age"];
}

@end

运行结果
2019-06-28 11:43:00.233200+0800 KVC底层原理(设值取值原理)[9049:29783240]
 *** Terminating app due to uncaught exception 'NSUnknownKeyException', 
reason: '[ setValue:forUndefinedKey:]: 
this class is not key value coding-compliant for the key age.'

2>返回值为YES,首先会_age成员变量赋值


_age赋值

接着会给_isAge赋值


_isAge赋值

再接着会给age赋值
age赋值

最后会给isAge赋值


isAge赋值

如果连四个成员变量都找不到,还是会崩溃然后报错NSUnknownKeyException
四个成员变量都没有

KVC取值原理

我们再来看KVC的取值原理流程图,valueForKey:内部调用流程

  • 1.按照getKey、key、isKey、_isKey顺序查找方法,找到方法了直接调用方法。
  • 2.第一步没找到就会调用accessInstanceVariablesDirectly方法,该方法返回值为NO时直接调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException,方法返回值是YES的时候进入第三步。该方法默认值是返回YES。
  • 3.按照_key、_isKey、key、isKey顺序查找成员变量,找到了就直接取值,没找到依然是调用setValue:forUndefinedKey:并抛出异常NSUnknownKeyException。


    KVC取值原理图.png

接下来我们同样验证一下以上流程

  1. 首先在Person类.h中不定义属性,只在.m文件实现getAge、age、isAge、_age方法,此时在ViewController类中创建Person的实例对象person,通过KVC取age值。
    运行程序,首先调用getAge方法
@implementation Person

- (int)getAge{
    return 10;
}

- (int)age{
    return 11;
}

- (int)isAge{
    return 12;
}

- (int)_age{
    return 13;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    
    NSLog(@"%@",[person valueForKey:@"age"]);
}

@end

打印结果
2019-06-28 14:39:33.612821+0800 KVC底层原理(设值取值原理)[10887:30101949] 10

然后依次注释getAge方法,运行打印结果为11,代表接下来会执行age方法;
再注释age方法,运行打印结果为12,代表接下来会执行isAge方法;
再注释isAge方法,运行打印结果是13,代表接下来执行的是_age方法。

  1. 接下来同样在Person.h中定义age,_isAge,age,isAge四个成员变量,并且实现accessInstanceVariablesDirectly方法,分别研究在返回值为YES和NO的流程。

1>返回值为NO,直接崩溃报错NSUnknownKeyException

@interface Person : NSObject
{
    int _age;
    int _isAge;
    int age;
    int isAge;
}

@end

@implementation Person

//- (int)getAge{
//    return 10;
//}
//
//- (int)age{
//    return 11;
//}
//
//- (int)isAge{
//    return 12;
//}
//
//- (int)_age{
//    return 13;
//}

+ (BOOL)accessInstanceVariablesDirectly{
    return NO;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    
    NSLog(@"%@",[person valueForKey:@"age"]);
}

@end

2>返回值为YES,首先会取_age成员变量的值

@interface Person : NSObject
{
    @public
    int _age;
    int _isAge;
    int age;
    int isAge;
}

@end

@implementation Person

//- (int)getAge{
//    return 10;
//}
//
//- (int)age{
//    return 11;
//}
//
//- (int)isAge{
//    return 12;
//}
//
//- (int)_age{
//    return 13;
//}

+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    person->_age = 10;
    person->_isAge = 11;
    person->age = 12;
    person->isAge = 13;
    
    NSLog(@"%@",[person valueForKey:@"age"]);
}

@end

打印结果
2019-06-28 14:56:30.994833+0800 KVC底层原理(设值取值原理)[11104:30169895] 10

然后依次注释_age属性以及赋值代码,运行打印结果为11,代表接下来会取_isAge的值;
再注释_isAge属性以及赋值代码,运行打印结果为12,代表接下来会取age的值;
再注释age属性以及赋值代码,运行打印结果为13,代表接下来会取isAge的值;
最后连isAge属性也注释掉,运行直接崩溃报错NSUnknownKeyException

面试题

1.通过KVC修改属性,会触发KVO吗?
答:是会触发KVO的。

以下通过代码验证一下
如果以下验证有不明白的地方,可以结合上篇文章KVO底层原理

1>通过KVC改变属性的值,答案是可以触发KVO的。因为定义属性,系统会自动生成set方法,而调用了set方法,自然就会触发KVO。

@interface Person : NSObject

@property (nonatomic, assign) int age;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    
    [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
    [person setValue:@10 forKey:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"监听到属性值%@的变化 - %@ - %@",keyPath,object,change);
}

打印结果
2019-06-28 15:37:48.288540+0800 KVC修改属性是否触发KVO[11731:30311788] 监听到属性值age的变化 -  - {
    kind = 1;
    new = 10;
    old = 0;
}

2>通过KVC直接改变成员变量的值,答案也是可以触发KVO。

@interface Person : NSObject{
    @public
    int _age;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    
    [person addObserver:self forKeyPath:@"_age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
    [person setValue:@10 forKey:@"_age"];

    [person removeObserver:self forKeyPath:@"_age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"监听到属性值%@的变化 - %@ - %@",keyPath,object,change);
}

@end

打印结果
2019-06-28 15:50:11.932356+0800 KVC修改属性是否触发KVO[11881:30355219] 监听到属性值_age的变化 -  - {
    kind = 1;
    new = 10;
    old = 0;
}

上一篇文章KVO的底层原理我们说过,直接修改成员变量的值,因为不会调用set方法,所以是不会调用KVO的。为什么现在使用KVC修改成员变量的值就可以触发KVO呢?我推测是因为KVC内部调用了willChangeValueForKey:和didChangeValueForKey:方法手动触发了KVO。

接下来我们在Person的.m文件中来重写这两个方法,验证一下是否内部真的有调用,如果有调用,则我们的猜想成立。

@interface Person : NSObject{
    @public
    int _age;
}
@end

@implementation Person

- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey - %@",key);
}

- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"didChangeValueForKey - begin - %@",key);
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - end - %@",key);
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    
    [person addObserver:self forKeyPath:@"_age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
    [person setValue:@10 forKey:@"_age"];
    
    [person removeObserver:self forKeyPath:@"_age"];
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"监听到属性值%@的变化 - %@ - %@",keyPath,object,change);
}

@end

打印结果
2019-06-28 16:00:41.718526+0800 KVC修改属性是否触发KVO[12005:30388256] willChangeValueForKey - _age
2019-06-28 16:00:41.718719+0800 KVC修改属性是否触发KVO[12005:30388256] didChangeValueForKey - begin - _age
2019-06-28 16:00:41.718855+0800 KVC修改属性是否触发KVO[12005:30388256] 监听到属性值_age的变化 -  - {
    kind = 1;
    new = 10;
    old = 0;
}
2019-06-28 16:00:41.718943+0800 KVC修改属性是否触发KVO[12005:30388256] didChangeValueForKey - end - _age

你可能感兴趣的:(KVC底层原理(设置取值原理))