iOS底层原理 - 探寻KVC本质

面试题引发的思考:

Q: KVC的赋值和取值过程是怎样的?原理是什么?

KVC的赋值过程:

  • 优先调用setKey:方法;
  • 如果setKey:方法不存在,则调用_setKey:方法;
  • 如果setKey:_setKey:方法都不存在,则调用accessInstanceVariablesDirectly方法,返回值为YES时,按照_key_isKeykeyisKey顺序查找成员变量。

KVC的取值过程:

  • 优先按照getKeykeyisKey_key顺序查找方法;
  • 如果以上方法方法都不存在,则调用accessInstanceVariablesDirectly方法,返回值为YES时,按照_key_isKeykeyisKey顺序查找成员变量。

Q: 通过KVC修改属性会触发KVO么?


1. KVC介绍

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

常见API:

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

使用方法如下:

// TODO: -----------------  Cat类  -----------------
@interface Cat : NSObject
@property (nonatomic, copy) NSString *name;
@end

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

// TODO: -----------------  ViewController类  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person = [[Person alloc] init];
    
    [person setValue:@10 forKey:@"age"];
    NSLog(@"age - %@", [person valueForKey:@"age"]);
    
    person.cat = [[Cat alloc] init];
    [person setValue:@"miaomiao" forKeyPath:@"cat.name"];
    NSLog(@"name - %@", [person valueForKeyPath:@"cat.name"]);
}

// 打印结果
Demo[1234:567890] age - 10
Demo[1234:567890] name - miaomiao

2. KVC赋值原理

setValue: forKey: 原理

KVC赋值过程如上图,验证方法如下:

// TODO: -----------------  Person类  -----------------
@interface Person : NSObject {
    @public
    // 按照`_key`、`_isKey`、`key`、`isKey`顺序查找成员变量
    int _age;
    int _isAge;
    int age;
    int isAge;
}
@end

@implementation Person
// 优先调用`setAge:`方法
- (void)setAge:(int)age {
    NSLog(@"setAge - %d", age);
}
// 如果`setAge:`方法不存在,则调用`_setAge:`方法
- (void)_setAge:(int)age {
    NSLog(@"_setAge - %d", age);
}
// 如果`setAge:`、`_setAge:`方法都不存在,则调用`accessInstanceVariablesDirectly`方法,查找成员变量
+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}
@end

// TODO: -----------------  ViewController类  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person = [[Person alloc] init];
    // 通过KVC修改age属性
    [person setValue:@10 forKey:@"age"];
}

通过KVC修改age属性时:

  • 优先调用setAge:方法;
  • 如果setAge:方法不存在,则调用_setAge:方法;
  • 如果setAge:_setAge:方法都不存在,则调用accessInstanceVariablesDirectly方法,返回值为YES时,按照_age_isAgeageisAge顺序查找成员变量。
成员变量顺序查找

以上结论皆可通过注释代码、断点得到,此文不做详细介绍。


3. KVC取值原理

valueForKey: 原理

KVC取值过程如上图,验证方法如下:

// TODO: -----------------  ViewController类  -----------------
@interface Person : NSObject {
    @public
    // 按照`_key`、`_isKey`、`key`、`isKey`顺序查找成员变量
    int _age;
    int _isAge;
    int age;
    int isAge;
}
@end

@implementation Person
// 按照`getKey`、`key`、`isKey`、`_key`顺序查找方法
- (int)getAge {
    return 11;
}
- (int)age {
    return 12;
}
- (int)isAge {
    return 13;
}
- (int)_age {
    return 14;
}
// 如果以上方法都不存在,则调用`accessInstanceVariablesDirectly`方法,查找成员变量
+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}
@end

// TODO: -----------------  ViewController类  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person = [[Person alloc] init];
    person->_age = 11;
    person->_isAge = 12;
    person->age = 13;
    person->isAge = 14;
    NSLog(@"%@", [person valueForKey:@"age"]);
}

通过KVC获取age属性时:

  • 优先按照getAgeageisAge_age顺序查找方法;
  • 如果以上方法方法都不存在,则调用accessInstanceVariablesDirectly方法,返回值为YES时,按照_key_isKeykeyisKey顺序查找成员变量。

以上结论皆可通过注释代码、断点得到,此文不做详细介绍。


4. 验证:通过KVC修改属性会触发KVO?

// TODO: -----------------  Person类  -----------------
@interface Person : NSObject {
    @public
    int _age;
}
@end

@implementation Person
+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}
- (void)willChangeValueForKey:(NSString *)key {
    NSLog(@"willChangeValueForKey - begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey - end");
}
- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"didChangeValueForKey - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - end");
}
@end

// TODO: -----------------  ViewController类  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person = [[Person alloc] init];
    // 添加KVO监听
    [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld |NSKeyValueObservingOptionNew context:NULL];
    // 通过KVC修改age的属性
    [person setValue:@10 forKey:@"age"];
    // 移除KVO监听
    [person removeObserver:self forKeyPath:@"age"];
}

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

// 打印结果
Demo[1234:567890] willChangeValueForKey - begin
Demo[1234:567890] willChangeValueForKey - end
Demo[1234:567890] didChangeValueForKey - begin
Demo[1234:567890] observeValueForKeyPath - {
    kind = 1;
    new = 10;
    old = 0;
}
Demo[1234:567890] didChangeValueForKey - end

由打印结果可知:

通过KVC修改age属性,并没有调用setAge:方法,而是直接修改的成员变量_age
但是如此也能触发监听者的observeValueForKeyPath: ofObject: change: context:方法。

说明通过KVC修改age属性时,会调用willChangeValueForKey:方法和didChangeValueForKey:方法。

那么可以证明:

通过KVC修改属性会触发KVO

你可能感兴趣的:(iOS底层原理 - 探寻KVC本质)