Objective-C基础之二(深入理解KVO、KVC)

2,243 阅读10分钟

KVO

什么是KVO

KVO的全称是key-value Observng,也叫做“键值监听”,通常用来监听某个对象的某个属性值的变化。下面使用一个简单的例子来回顾一下KVO的用法。

  • 创建一个XLPerson类,内部创建一个name和age属性
@interface XLPerson : NSObject

@property(nonatomic, copy)NSString *name;
@property(nonatomic, assign)int age;

@end
  • 在ViewController中监听XLPerson的age属性
@implementation ViewController

- (void)viewDidLoad{
    [super viewDidLoad];

    self.person = [[XLPerson alloc] init];
    self.person.age = 10;
    
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.age = 20;
}

@end
  • person对象的age属性初始时设置为10,当点击屏幕,设置age为20时,系统自动触发了observeValueForKeyPath,打印出了age的旧值和新值,如下
2019-11-13 14:52:49.960452+0800 TestFont[52476:1429894] 
 keyPath:age, 
 object:<XLPerson: 0x6000039bec00>, 
 change:{
    kind = 1;
    new = 20;
    old = 10;
}

KVO内部实现

结合之前对NSObject底层的学习我们知道,实例对象的isa指针指向它的类对象,那么上文例子中的person对象的isa指针应该指向它的类对象XLPerson,为了做对比,我们增加一个person2对象:

- (void)viewDidLoad{
    [super viewDidLoad];

    self.person = [[XLPerson alloc] init];
    self.person.age = 10;
    
    self.person2 = [[XLPerson alloc] init];
    self.person2.age = 30;
    
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.age = 20;
}

@end

在touchesBegan方法中添加断点,然后我们使用LLDB命令来对代码进行调试

(lldb) p self.person->isa
(Class) $1 = NSKVONotifying_XLPerson
(lldb) p self.person2->isa
(Class) $2 = XLPerson
(lldb) 

这时候会发现添加了Observer后的person对象的isa指针不是指向XLPerson,而是指向一个新的类对象NSKVONotifying_XLPerson,而person2对象由于没有添加Observer,所以它的isa指针指向的是类对象XLPerson。

由于我们并没有创建过NSKVONotifying_XLPerson类,所以NSKVONotifying_XLPerson是在运行时动态生成的一个新的类,新类生成之后,又将person的isa指针指向了新的类对象。

为了了解NSKVONotifying_XLPerson的内部构造,我们自定义一个方法来打印Class的方法列表和superClass

- (void)descriptionOfClass:(Class)cls{
    NSLog(@"------------ %@ -----------", NSStringFromClass(cls));
    NSLog(@"%@ superClass ----> %@", NSStringFromClass(cls),NSStringFromClass(class_getSuperclass(cls)));
    
    unsigned int count;
    Method *methondList = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; i++) {
        Method method = methondList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"%@ ----> %@", NSStringFromClass(cls), methodName);
    }
    free(methondList);
}

修改示例中的代码,打印出person和person1的方法列表和superClass

- (void)viewDidLoad{
    [super viewDidLoad];

    self.person = [[XLPerson alloc] init];
    self.person.age = 10;
    
    self.person2 = [[XLPerson alloc] init];
    self.person2.age = 20;
    
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
    [self descriptionOfClass:object_getClass(self.person)];
    NSLog(@"\n");
    [self descriptionOfClass:object_getClass(self.person2)];
}

注意:由于对象的实例方法都存放在类对象的methonList中,所以此处我们需要通过object_getClass方法拿到person和person1对象的类对象,然后通过遍历类对象的方法列表打印出具体的方法名称。 object_getClass方法如果传递过去一个示例对象,那么会返回对应的类对象,如果传递过去一个类对象,会返回对应的元类对象。

运行程序,得到以下运行结果

从图中可以看出,person对象由于加了KVO监听,所以它的类对象变成了NSKVONotifying_XLPerson,而NSKVONotifying_XLPerson对象的superClass是XLPerson,说明NSKVONotifying_XLPerson是XLPerson的子类。

在NSKVONotifying_XLPerson方法列表中主要有4个方法,setAge:、class、dealloc和_isKVOA,下面我们就来一一分析这四个方法。

  • NSKVONotifying_XLPerson重写了父类中的setAge:方法,在setAge:方法中调用了Foundation框架中的_NSSetIntValueAndNotify方法,而_NSSetIntValueAndNotify方法就执行了监听KVO的核心逻辑,伪代码如下:
- (void)setAge:(int)age{
    //调用Foundationf框架中的_NSSetIntValueAndNotify方法
    [self _NSSetIntValueAndNotify];
}

- (void)_NSSetIntValueAndNotify{
    //将要修改age的值
    [self willChangeValueForKey:@"age"];
    //调用父类的setAge方法去修改age的值
    [super setAge:age];
    //完成修改age的值,并且执行observeValueForKeyPath方法
    [self didChangeValueForKey:@"age"];
}
  • NSKVONotifying_XLPerson会重写父类的class方法,原因是Apple不想让调用者知道NSKVONotifying_XLPerson这个中间类的存在,所以重写class,返回原类的class对象,伪代码如下
- (Class)class{
    return [XLPerson class];
}
  • 当NSKVONotifying_XLPerson类被销毁的时候,dealloc方法就被用来做一些收尾工作
  • _isKVOA则是用来标识当前类是否是通过runtime动态生成的类对象,如果是,就返回YES,不是,则返回NO

还原NSKVONotifying_XLPerson对象的内部构造

上文介绍了NSKVONotifying_XLPerson对象中的几个主要的方法,现在我们就来还原一下NSKVONotifying_XLPerson对象完整的内部结构。

首先,NSKVONotifying_XLPerson是Class类型的对象,所以它内部肯定拥有isa指针和superClass指针,由此可以得到NSKVONotifying_XLPerson的结构如下:

结合isa指针的指向可以得到以下结构:

由此也可以得到NSKVONotifying_XLPerson的伪代码如下


@interface NSKVONotifying_XLPerson : XLPerson

@end

@implementation NSKVONotifying_XLPerson

- (void)setAge:(int)age{
    //调用Foundationf框架中的_NSSetIntValueAndNotify方法
    [self _NSSetIntValueAndNotify];
}

- (void)_NSSetIntValueAndNotify{
    //将要修改age的值
    [self willChangeValueForKey:@"age"];
    //调用父类的setAge方法去修改age的值
    [super setAge:age];
    //完成修改age的值,并且执行observeValueForKeyPath方法
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key{
    //触发observeValueForKeyPath方法
    [self observeValueForKeyPath:@"age" ofObject:self change:nil context:nil];
}

- (void)dealloc{
    //释放操作
}

- (Class)class{
    return [XLPerson class];
}

- (BOOL)_isKVOA{
    return YES;
}

@end

KVO总结

  • 首先,给一个实例对象添加KVO,内部是利用Runtime动态的生成一个此实例对象的类对象的子类,具体的格式为_NSKVONotifying_XXX,并且让实例对象的isa指针指向这个新生成的类。
  • 重写属性的set方法,当调用set方法时,会调用Foundation框架的NSSetXXXValueAndNotify函数
  • 在_NSSetXXXValueAndNotify中会执行一下步骤
    • 调用willChangeValueForKey:方法
    • 调用父类的set方法,重新赋值
    • 调用didChangeValueForKey:方法,didChangeValueForKey:内部会触发监听器的observeValueForKeyPath:ofObject:change:context:方法

KVC

什么是KVC?

KVC,俗称“键值编码”,全称是“Key Value Coding”,它是一种可以直接通过字符串的名称(Key)来访问类属性的机制,而不是通过调用Setter或者Getter方法来进行访问。

KVC的常用方法如下

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

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

KVC有两种赋值和取值方法,下面我们通过一个简单的例子来了解一下。首先创建XLPerson类和XLStudent类


@interface XLStudent : NSObject

@property(nonatomic, assign)int num;

@end

@interface XLPerson : NSObject

@property(nonatomic, strong)XLStudent *student;
@property(nonatomic, copy)NSString *name;
@property(nonatomic, assign)int age;

@end

然后通过KVC来设置XLPerson和XLStudent的属性的值,如下

- (void)viewDidLoad{
    [super viewDidLoad];

    XLPerson *person = [[XLPerson alloc] init];
    
    [person setValue:[[XLStudent alloc] init] forKey:@"student"];
    [person setValue:@10 forKey:@"age"];
    [person setValue:@"张三" forKey:@"name"];
    [person setValue:@20 forKeyPath:@"student.num"];
    
    NSNumber *age = [person valueForKey:@"age"];
    NSString *name = [person valueForKey:@"name"];
    NSNumber *num = [person valueForKeyPath:@"student.num"];
    
    NSLog(@"%@, %@, %@",age,name,num);
}

最后得到结果age=10、name=张三、num=20,由此可见,通过KVC确实可以修改对象中的属性。

使用KVC除了可以修改属性,也可以修改成员变量的值,在XLPerson中增加如下成员变量

@interface XLPerson : NSObject{
    int _height;
    int _weight;
}
@end

然后使用KVC进行赋值

XLPerson *person = [[XLPerson alloc] init];
[person setValue:@30 forKey:@"_height"];
[person setValue:@40 forKeyPath:@"_weight"];

NSLog(@"%@, %@",person->_height,person->_weight);

最后可以发现KVC确实也能修改成员变量的值。

同时,通过上面的代码我们可以看出两种赋值和取值方法的区别。

  • setValue:forkey可以给person对象的所有属性赋值,但是层级只有一级,如果存在多级属性赋值,那么就需要调用多次此方法,上文的例子中,如果要修改student对象的num属性,就必须调用两次
[[person valueForKey:@"student"] valueForKey:@"num"];
  • setValue:forkeyPath:支持一级属性赋值,也支持多级属性赋值,需要将属性的具体访问路径传递过去,在上文的例子中,通过student.num就可以修改student对象的num属性。在使用上更加简洁。

KVC底层原理

setValue:forkey:赋值流程

其实通过setValue:forkey方法给对象的属性赋值,主要经过以下几个流程

  • 首先会按照setKey:、_setKey:的顺序到对象的方法列表中寻找这两个方法,如果找到了方法,则传参并且调用方法。
  • 如果没有找到方法,则通过accessInstanceVariablesDirectly方法的返回值来决定是否能够查找成员变量。如果accessInstanceVariablesDirectly返回YES,则会按照以下顺序到成员变量列表中查找对应的成员变量:
    • _key
    • _isKey
    • key
    • isKey
  • 如果accessInstanceVariablesDirectly返回NO,则直接抛出NSUnknownKeyException异常。
  • 如果在成员变量列表中找到对应的属性值,则直接进行赋值,如果找不到,则会抛出NSUnknownKeyException异常。

对应流程图如下:

valueForKey:取值流程

通过valueForKey:方法取值,流程如下:

  • 首先会按照以下顺序查找方法列表
    • getKey
    • key
    • isKey
    • _key
  • 如果找到就直接传递参数,调用方法,如果未找到则查看accessInstanceVariablesDirectly方法的返回值,如果返回NO,则直接抛出NSUnknownKeyException异常
  • 如果accessInstanceVariablesDirectly方法返回YES,则按如下顺序查找成员变量列表
    • _key
    • _isKey
    • key
    • isKey
  • 如果能找到对应的成员变量,则直接获取成员变量的值,如果未找到,则抛出NSUnknownKeyException异常

流程图如下:

KVC和KVO的联系

通过对KVO的探索,我们知道,给对象的某个属性添加KVO监听,其实是动态创建了一个此类的子类,然后将对象的isa指针指向新生成的类,最后通过重写属性的setter方法来添加监听。那么如果使用KVC来对属性或者成员变量进行赋值,会触发KVO监听吗?我们通过一个简单的例子来测试一下

还是使用上文的XLPerson对象

- (void)viewDidLoad{
    [super viewDidLoad];

    self.person = [[XLPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    [self.person addObserver:self forKeyPath:@"_height" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

}

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

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person setValue:@10 forKey:@"age"];
    [self.person setValue:@20 forKeyPath:@"_height"];
}

运行代码,点击屏幕可以看到如下打印信息

通过KVC不管是设置属性的值还是成员变量的值,都会触发KVO监听,说明在KVC内部确实会在给属性或成员变量赋值的时候,会通过类似调用didChangeValueForKey方法来触发KVO监听。

面试题

KVO

KVO的本质是什么?

  • 给一个实例对象添加KVO,系统内部是利用Runtime动态的生成一个此实例对象的类对象的子类,具体的格式为_NSKVONotifying_XXX,并且让实例对象的isa指针指向这个新生成的类。
  • 重写属性的set方法,当调用set方法时,会调用Foundation框架的NSSetXXXValueAndNotify函数
  • 在_NSSetXXXValueAndNotify中会执行以下步骤
    • 调用willChangeValueForKey:方法
    • 调用父类的set方法,重新赋值
    • 调用didChangeValueForKey:方法,didChangeValueForKey:内部会触发监听器的observeValueForKeyPath:ofObject:change:context:方法

如何手动触发KVO?

在修改变量前后手动调用willChangeValueForKey:和didChangeValueForKey:方法

[self willChangeValueForKey:name];
_name = @"xxx";
[self didChangeValueForKey:name];

直接修改成员变量的值是否会触发KVO?

直接修改成员变量的值不会触发KVO,因为没有触发setter方法。

KVC

通过KVC修改属性的值会触发KVO吗?

会触发KVO。

KVC的赋值过程和取值过程分别是什么样的?

参考上文流程图

KVC的原理是什么?

参考上文流程图

结束语

以上内容纯属个人理解,如果有什么不对的地方欢迎留言指正。

一起学习,一起进步~~~