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

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对象:bash

- (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是在运行时动态生成的一个新的类,新类生成以后,又将personisa指针指向了新的类对象。学习

为了了解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);
}
复制代码

修改示例中的代码,打印出personperson1的方法列表和superClassui

- (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方法若是传递过去一个示例对象,那么会返回对应的类对象,若是传递过去一个类对象,会返回对应的元类对象。编码

运行程序,获得如下运行结果atom

从图中能够看出,person对象因为加了KVO监听,因此它的类对象变成了NSKVONotifying_XLPerson,而NSKVONotifying_XLPerson对象的superClassXLPerson,说明NSKVONotifying_XLPersonXLPerson的子类。spa

NSKVONotifying_XLPerson方法列表中主要有4个方法,setAge:classdealloc_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_XLPersonClass类型的对象,因此它内部确定拥有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来设置XLPersonXLStudent的属性的值,以下

- (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=10name=张三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方法来添加监听。那么若是使用KVO来对属性或者成员变量进行赋值,会触发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的原理是什么?

参考上文流程图

结束语

以上内容纯属我的理解,若是有什么不对的地方欢迎留言指正。

一块儿学习,一块儿进步~~~

相关文章
相关标签/搜索