KVO
和KVC
, 机器底层是如何实现的KVO
的全称是Key-Value Observing
,俗称键值监听,能够用于监听某个对象属性值的改变KVO
是使用获取其余对象的特定属性变化的通知机制,控制器层的绑定技术就是严重依赖键值观察得到模型层和控制器层的变化通知的KVC
和KVO
都是基于OC
的动态特性和Runtime
机制的以下所示, 咱们为person
对象添加一个监听html
- (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<UITouch *> *)touches withEvent:(UIEvent *)event { self.person.age = 10; } // 当监听的对象发生改变时就会调用 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { } 复制代码
上面添加监听的方法微信
addObserver:forKeyPath:options:context: 监听方法各个参数的做用分别是什么 [object addObserver: observer forKeyPath: @"frame" options: 0 context: nil]; /** object: 被观察者 observer: 观察者 KeyPath: 被观察者索贝观察的属性 options: 有四个值 一、NSKeyValueObservingOptionNew 把更改以前的值提供给处理方法 二、NSKeyValueObservingOptionOld 把更改以后的值提供给处理方法 三、NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注 册,立马就会调用一次。一般它会带有新值,而不会带有旧值。 四、NSKeyValueObservingOptionPrior 分2次调用。在值改变以前和值改变以后。 context:上下文,能够带一些参数,任何类型均可以 */ 复制代码
当被监听的对象的属性发生改变时就会调用下面的方法markdown
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { } /* 1. keyPath: 被监听的属性 2. object: 被监听的对象 3. change 属性变化字典(新/旧) 4. 上下文,与监听的时候传递的一致 */ 复制代码
这里咱们建立两个pweson
对象, 可是只对person1
实行监听oop
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
监听的person1
的isa
变成了NSKVONotifying_Person
NSKVONotifying_Person
这个类是由Runtime
在运行状态下动态建立的一个类, 是Person
的一个子类age
属性进行赋值操做的时候, 其实调用的是Person
类的setAge
方法
person1
经过isa
找到其对应的类对象Person
类, 并调用Person
类的setAge
方法person2
经过isa
找到其对应的类对象NSKVONotifying_Person
类, 并调用NSKVONotifying_Person
类的setAge
方法setAge
方法的实现是不同的, 后面会详解Person
和NSKVONotifying_Person
对应的类对象以下所示使用了KVO
监听的对象动态生成的NSKVONotifying_Person
类编码
实际上NSKVONotifying_Person
类中的setAge:
方法内部是调用了Foundation
的_NSSetIntValueAndNotify
方法, 有兴趣的能够反编译一下Foundation.framwork
的源码, 查看其伪代码, 大体的能够推出内部方法的实现, 代码大体以下atom
- (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
其实重写了willChangeValueForKey
和didChangeValueForKey
两个方法didChangeValueForKey
方法中实现的首先咱们在
Person
类内部重写willChangeValueForKey
和didChangeValueForKey
两个方法, 在运行的过程当中分别加断点进行调试, 以下spa
- (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<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context); } 复制代码
在输出结果中能够看到代码的执行顺序, 从下面的代码能够看出监听属性的改变实际上是在didChangeValueForKey
方法中实现的code
setAge: didChangeValueForKey - begin 监听到<MJPerson: 0x60000389b680>的age属性值改变了 didChangeValueForKey - end 复制代码
KVC
全称是Key Value Coding
(键值编码),是一个基于NSKeyValueCoding
非正式协议实现的机制,它能够直接经过key
值对对象的属性进行存取操做,而不需经过调用明确的存取方法KVC
提供了一种间接访问属性方法或成员变量的机制,能够经过字符串来访问对象的的属性方法或成员变量// 通用的访问方法 - (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 <Foundation/Foundation.h> @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
文件中没有默认生成的setAge
和getAge
方法setValue
方法对age
存值的时候就会致使程序崩溃, 并会报出setValue:forUndefinedKey:]
的错误setValue:forKey:
的原理实际上就是先按照setAge:
和_setAge:
顺序查找方法, 若是找到了对应方法中的一个, 则代码能够执行成功, 下面咱们就一个个验证一下吧1. 验证setKey
和_setKey
方法, 代码以下
#import <Foundation/Foundation.h> 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 <Foundation/Foundation.h> @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
属性, 以此类推依次是age
和isAge
变量, 有兴趣的能够亲自测试一番valueForKey
valueForKey
经过key
进行取值的时候, 取值流程和setValue
相似, 途中也比较清晰, 这里就不在赘述了
欢迎您扫一扫下面的微信公众号,订阅个人博客!