博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
探索KVC和KVO的本质
阅读量:7226 次
发布时间:2019-06-29

本文共 7094 字,大约阅读时间需要 23 分钟。

  • 原文链接:
  • 这篇文章主要介绍KVOKVC, 机器底层是如何实现的
  • KVO的全称是Key-Value Observing,俗称键值监听,可以用于监听某个对象属性值的改变
  • KVO是使用获取其他对象的特定属性变化的通知机制,控制器层的绑定技术就是严重依赖键值观察获得模型层和控制器层的变化通知的
  • 对于不依赖控制器层类的应用程序,键值观察提供了一种简化的方法来实现检查器并更新用户界面值
  • KVCKVO都是基于OC的动态特性和Runtime机制的

KVO

添加监听

如下所示, 我们为person对象添加一个监听

- (void)viewDidLoad {    [super viewDidLoad];        self.person = [[Person alloc]init];    self.person.age = 10;        // 给person添加KVO监听    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;    [self.person addObserver:self forKeyPath:@"age" options:options context:nil];}- (void)touchesBegan:(NSSet
*)touches withEvent:(UIEvent *)event { self.person.age = 10;}// 当监听的对象发生改变时就会调用- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary
*)change context:(void *)context { }复制代码

上面添加监听的方法

addObserver:forKeyPath:options:context:监听方法各个参数的作用分别是什么[object addObserver: observer forKeyPath: @"frame" options: 0 context: nil];/**object: 被观察者observer: 观察者KeyPath: 被观察者索贝观察的属性options: 有四个值    1、NSKeyValueObservingOptionNew 把更改之前的值提供给处理方法    2、NSKeyValueObservingOptionOld 把更改之后的值提供给处理方法    3、NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注  册,立马就会调用一次。通常它会带有新值,而不会带有旧值。    4、NSKeyValueObservingOptionPrior 分2次调用。在值改变之前和值改变之后。context:上下文,可以带一些参数,任何类型都可以*/复制代码

当被监听的对象的属性发生改变时就会调用下面的方法

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary
*)change context:(void *)context { }/* 1. keyPath: 被监听的属性 2. object: 被监听的对象 3. change 属性变化字典(新/旧) 4. 上下文,与监听的时候传递的一致 */复制代码

KVO的本质

这里我们创建两个pweson对象, 但是只对person1实行监听

self.person1 = [[Person alloc]init];    self.person2 = [[Person alloc]init];    self.person1.age = 10;    self.person2.age = 10;        // 给person添加KVO监听    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;    [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];复制代码

下面我们可以在touchesBegan方法中分别添加断点打印两个对象的isa, 如下

  • 从上面可以看出,未添加监听的pweson2对象的isa依然是Person, 但是添加KVO监听的person1isa变成了NSKVONotifying_Person
  • NSKVONotifying_Person这个类是由Runtime在运行状态下动态创建的一个类, 是Person的一个子类
  • 当我们对age属性进行赋值操作的时候, 其实调用的是Person类的setAge方法
    • person1通过isa找到其对应的类对象Person类, 并调用Person类的setAge方法
    • person2通过isa找到其对应的类对象NSKVONotifying_Person类, 并调用NSKVONotifying_Person类的setAge方法
    • 两个类的setAge方法的实现是不一样的, 后面会详解
  • PersonNSKVONotifying_Person对应的类对象如下所示

使用了KVO监听的对象动态生成的NSKVONotifying_Person

实际上NSKVONotifying_Person类中的setAge:方法内部是调用了Foundation_NSSetIntValueAndNotify方法, 有兴趣的可以反编译一下Foundation.framwork的源码, 查看其伪代码, 大致的可以推出内部方法的实现, 代码大致如下

- (void)setAge:(int)age{    _NSSetIntValueAndNotify();}// 伪代码void _NSSetIntValueAndNotify(){    [self willChangeValueForKey:@"age"];    [super setAge:age];    [self didChangeValueForKey:@"age"];}- (void)didChangeValueForKey:(NSString *)key{    // 通知监听器,某某属性值发生了改变    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];}复制代码
  • 从上面的代码可以看出_NSSetIntValueAndNotify其实重写了willChangeValueForKeydidChangeValueForKey两个方法
  • 而且监听属性值变化的是在didChangeValueForKey方法中实现的
  • 下面我们就来验证一下上述代码

首先我们在Person类内部重写willChangeValueForKeydidChangeValueForKey两个方法, 在运行的过程中分别加断点进行调试, 如下

- (void)setAge:(int)age{    _age = age;        NSLog(@"setAge:");}- (void)willChangeValueForKey:(NSString *)key{    [super willChangeValueForKey:key];        NSLog(@"willChangeValueForKey");}- (void)didChangeValueForKey:(NSString *)key{    NSLog(@"didChangeValueForKey - begin");        [super didChangeValueForKey:key];        NSLog(@"didChangeValueForKey - end");}复制代码

然后在如下代码中加断点

// 当监听对象的属性值发生改变时,就会调用- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary
*)change context:(void *)context{ NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);}复制代码

在输出结果中可以看到代码的执行顺序, 从下面的代码可以看出监听属性的改变其实是在didChangeValueForKey方法中实现的

setAge:didChangeValueForKey - begin监听到
的age属性值改变了 didChangeValueForKey - end复制代码

KVC

  • KVC全称是Key Value Coding(键值编码),是一个基于NSKeyValueCoding非正式协议实现的机制,它可以直接通过key值对对象的属性进行存取操作,而不需通过调用明确的存取方法
  • 这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定
  • KVC提供了一种间接访问属性方法或成员变量的机制,可以通过字符串来访问对象的的属性方法或成员变量
  • 相关常见的API有
// 通用的访问方法- (id)valueForKey:(NSString *)key; - (void)setValue:(id)value forKey:(NSString *)key;// 衍生的keyPath方法, 用来进行深层访问(key使用点语法),也可单层访问:- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;- (id)valueForKeyPath:(NSString *)keyPath;复制代码

通用访问方法使用示例

// 使用示例Person *person = [[Person alloc] init];  // 赋值[person setValue:@"titan" forKey:@"name"];// 取值NSLog(@"-------name = %@",person.name);NSLog(@"-------name = %@",[person valueForKey:@"name"]);复制代码

keyPath方法使用示例

//注意,这里要想使用keypath对adress的属性进行赋值,必须先给myself赋一个Address对象Address *myAddress = [[Address alloc] init];   [myself setValue:myAddress forKey:@"address"];   //KeyPath为多级访问[myself setValue:@"rizhao" forKeyPath:@"address.city"]; //取值NSLog(@"-------city = %@",myself.address.city);NSLog(@"-------city = %@",[myself valueForKeyPath:@"address.city"]);复制代码

底层原理

setValue:forKey:

0. 我们先创建一个Person类, 并在Person.h文件中声明一个age属性, 如下

#import 
@interface Person : NSObject@property (assign, nonatomic) int age;@end复制代码

下面我们在ViewController.m里面调用一下看看

- (void)viewDidLoad {    [super viewDidLoad];        Person *person = [[Person alloc]init];    // 这种方式调用的是setAge方法    person.age = 10;        // 内部其实是调用的setAge方法    [person setValue:@20 forKey:@"age"];    NSLog(@"%d", person.age);    // 打印结果20}复制代码
  • 如果在Person.h文件中没有声明age属性,也就是在Person.m文件中没有默认生成的setAgegetAge方法
  • 那么调用setValue方法对age存值的时候就会导致程序崩溃, 并会报出setValue:forUndefinedKey:]的错误
  • 如同上图中所示, setValue:forKey:的原理实际上就是先按照setAge:_setAge:顺序查找方法, 如果找到了对应方法中的一个, 则代码可以执行成功, 下面我们就一个个验证一下吧

1. 验证setKey_setKey方法, 代码如下

#import 
NS_ASSUME_NONNULL_BEGIN@interface Person : NSObject// .h文件中不添加age属性//@property (assign, nonatomic) int age;@end复制代码

.m文件中分别添加一下两个方法, 侧其中一个方法的时候, 可以先注释掉另外一个方法

#import "Person.h"@implementation Person- (void)setAge:(int)age {    NSLog(@"setAge--");}- (void)_setAge:(int)age {        NSLog(@"_setAge--");}@end复制代码

然后在ViewController.m调用setValue方法的时候, 可以看到打印对应的输出, 当上述两个方法同事存在的时候, 则会默认执行setAge方法

[person setValue:@20 forKey:@"age"];复制代码

2. 如果没有setKey:_setKey:两个方法, 则会继续查找Person.m文件中是否有accessInstanceVariablesDirectly方法, 如果没有程序会奔溃

#import "Person.h"@implementation Person+ (BOOL)accessInstanceVariablesDirectly {    // 默认返回值是YES    return YES;}@end复制代码
  • accessInstanceVariablesDirectly方法默认是返回YES的, 如果return NO, 则程序同样会崩溃, 并抛出NSUnknownKeyException异常
  • return YES的情况下, 会按照顺序查找_key、_isKey、key、isKey等成员变量, 如果找不到依然会抛出NSUnknownKeyException异常
  • 下面在Person.h文件中, 分别声明四个变量
#import 
@interface Person : NSObject{ @public int age; int isAge; int _age; int _isAge;}@end复制代码

ViewController.m中添加如下代码, 执行结果如下所示

- (void)viewDidLoad {    [super viewDidLoad];        Person *person = [[Person alloc]init];    [person setValue:@20 forKey:@"age"];    NSLog(@"-----------");}复制代码

  • 当我们在Person.h中声明age、isAge、_age、_isAge四个变量的时候, 上述代码会默认赋值给_age变量
  • 当我们不声明_age属性时, 则会默认赋值给_isAge属性, 以此类推依次是ageisAge变量, 有兴趣的可以亲自测试一番

valueForKey

valueForKey通过key进行取值的时候, 取值流程和setValue类似, 途中也比较清晰, 这里就不在赘述了


转载于:https://juejin.im/post/5d00c95b51882571521110e0

你可能感兴趣的文章
forEach,for...of,map与asycn/await
查看>>
springboot 2 Hikari 多数据源配置问题(dataSourceClassName or jdbcUrl is required)
查看>>
Golang数据库编程之GORM模型定义与数据库迁移
查看>>
Oracle redo解析之-4、rowid的计算
查看>>
Easy Scheduler 1.0.3 发布,分布式工作流任务调度系统
查看>>
java 颠倒整数
查看>>
Python入门教程100天:Day05-练习总结
查看>>
环境搭建,8种基本类型,Static,package和import,log4j
查看>>
即将到来的 Debian 10 Buster 发布版的新特点
查看>>
iOS 头部视图下拉变大
查看>>
Disruptor并发框架
查看>>
react-hooks 实现简单的评论list
查看>>
【多图警告】学会JavaScript测试你就是同行中最亮的仔(妹)
查看>>
19-04-25
查看>>
一个JAVA程序员成长之路分享
查看>>
30K iOS程序员的简述:如何快速进阶成为高级开发人员
查看>>
Go 夜读 - 每周四晚上 Go 源码阅读技术分享
查看>>
tranform知多少
查看>>
Android电量优化
查看>>
[爬虫手记] 我是如何在3分钟内开发完一个爬虫的
查看>>