KVC & KVO

​ 前言:KVC和KVO在项目开发中很常用,所以还是有必要深入了解一下。本文先介绍了KVC的定义和常见的API及使用方法,并介绍了setValue: forKey和valueForKey的底层调用顺序。然后介绍了KVO常见的用法和注意事项;最后说明了一些面试官喜欢面试的KVC和KVO的细节点。

一、KVC-键值编码:

1、KVC定义:

​ KVC,Key-Value Coding,键值编码,开发者可以通过key直接访问对象的属性,或者给对象的属性赋值,是iOS中的黑魔法之一。

2、常见的API:
- (void)setValue:(id)value forKey:(NSString *)key; //根据属性名赋值
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath; //根据路径赋值
- (void)setValue:(id)value forUndefinedKey:(NSString *)key; //赋值找不到属性
- (id)valueForKey:(NSString *)key; //根据属性名取值
- (id)valueForKeyPath:(NSString *)keyPath; //根据路径取值
- (id)valueForUndefinedKey:(NSString *)key; //取值找不到属性

代码如下:先创建一个Person类和Dog类,

@class Dog;
@interface Person : NSObject
{
    @public
    int _age;
}
@property(nonatomic, assign) int age;
@property(nonatomic, copy) NSString *name;
@property(nonatomic, strong) Dog *dog;
@end

@interface Dog : NSObject
@property(nonatomic, copy) NSString *name;
@end

运行代码:

#import "ViewController.h"
#import "Person.h"

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [Person new];
    self.person.dog = [Dog new];
    
    // 如果key不存在,则抛出NSUnknownKeyException异常,可以重写UndefinedKey的两个方法
    [self.person setValue:@"18" forKey:@"age"];
    [self.person setValue:@"旺财" forKeyPath:@"dog.name"];
    
    int age = [[self.person valueForKey:@"age"] intValue];
    NSString *dogName = [self.person valueForKeyPath:@"dog.name"];
    
    NSLog(@"age = %d, %d", self.person.age, age);
    NSLog(@"dogName = %@, %@", self.person.dog.name, dogName);
}

@end

打印结果:

age = 18, 18
dogName = 旺财, 旺财
3、setValue: forKey和valueForKey的底层调用顺序:

1)setValue: forKey调用顺序:

先调用setter方法,如果没找到方法,查看accessInstanceVariablesDirectly的返回值,如果是YES,就查找成员变量_key _isKey key isKey 并赋值,如果返回值是NO,直接抛出异常,流程图如下:

setValue: forKey调用顺序

ps:accessInstanceVariablesDirectly方法的默认返回值是YES。

2)valueForKey调用顺序:

先调用getter等方法,如果没找到方法,查看accessInstanceVariablesDirectly的返回值,如果是YES,就查找成员变量_key _isKey key isKey 并取值,如果返回值是NO,直接抛出异常,流程图如下:

valueForKey调用顺序

二、KVO-键值监听:

1、KVO定义:

​ KVO,Key-Value Observing,键值监听,也叫键值观察,KVO是基于KVC实现的。它是一种观察者模式的衍生,其基本思想是,对目标对象的某个属性添加观察,当该属性发生变化时,通过触发观察者实现的KVO接口方法,来通知观察者。观察者模式较完美将目标对象与观察者对象解耦。

​ 简单来说,KVO可以通过监听key,来获取value的变化,用来在对象之间监听状态的变化。

2、常见的API:
/**
 添加观察者

 @param observer 观察者,一般是self
 @param keyPath 属性或者属性路径
 @param options 可选择新值和旧值等,可多选
 @param context 上下文,用来区分消息,可以为nil
 */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

/**
 移除观察者

 @param observer 观察者,一般是self
 @param keyPath 属性或者属性路径
 */
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

如上,先创建一个Person类和Dog类,运行代码:

#import "ViewController.h"
#import "Person.h"

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [Person new];
    self.person.dog = [Dog new];
    
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    [self.person addObserver:self forKeyPath:@"dog.name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
    self.person.age = 18;
    self.person.dog.name = @"旺财";
}

- (void)dealloc {
        // 注意移除KVO,否则会造成内存泄漏
    [self.person removeObserver:self forKeyPath:@"age"];
    [self.person removeObserver:self forKeyPath:@"dog.name"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (object == self.person) {
        if ([keyPath isEqualToString:@"age"]) {
            NSLog(@"--------KVO-Age------------");
            NSNumber *newAge = change[NSKeyValueChangeNewKey];
            NSNumber *oldAge = change[NSKeyValueChangeOldKey];
            NSLog(@"newAge = %@", newAge);
            NSLog(@"oldAge = %@", oldAge);
        } else if ([keyPath isEqualToString:@"dog.name"]) {
            NSLog(@"--------KVO-DogName--------");
            NSString *newDogName = change[NSKeyValueChangeNewKey];
            NSString *oldDogName = change[NSKeyValueChangeOldKey];
            NSLog(@"newDogName = %@", newDogName);
            NSLog(@"oldDogName = %@", oldDogName);
        }
    }
}

@end

打印结果:

--------KVO-Age------------
newAge = 18
oldAge = 0
--------KVO-DogName--------
newDogName = 旺财
oldDogName = <null>
3、KVO底层实现:

​ 先说结论:使用了KVO,系统通过runtime动态创建一个NSKVONotifying_Person派生类对象,并将person对象的isa指针指向该类,该类是Person的子类,该类重写了setter方法,在setter方法里调用了willChangeValueForKey和didChangeValueForKey,而didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法。

注意:派生类只重写注册了观察者的属性方法。

代码和断点如下:

断点位置

在打印区调试结果:

// 第一个断点,还没添加KVO,isa指针和class方法均返回Person类对象
(lldb) p object_getClassName(self.person)
(const char * _Nonnull) $0 = 0x000000010b7ac3a3 "Person"
(lldb) p [self.person class]
(Class) $0 = Person

// 第二个断点,添加KVO之后,isa指针指向NSKVONotifying_Person对象,class还是返回Person对象
(lldb) p object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x0000600000fecac0 "NSKVONotifying_Person"
(lldb) p [NSClassFromString(@"NSKVONotifying_Person") isSubclassOfClass:[Person class]]
(BOOL) $2 = YES
(lldb) p [self.person class]
(Class) $3 = Person

注意:由于派生类是重写了setter方法,所以直接修改成员变量的值不会触发KVO。

​ KVC会调用setter方法,所以KVC会自动触发KVO。

self.person->_age = 18; //不会触发KVO
self.person.dog.name = @"旺财"; //点调用,即调用setter方法,会触发KVO
[self.person setValue:@"饼哥IT" forKey:@"name"]; //KVC会触发KVO

觉得写的不错,有些启发或帮助,点个赞哦!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容