一、KVO介绍
KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
二、KVO实例
通过一个简单的例子来演示一下KVO,首先创建一个RevanPerson类,并且定义一个age属性,我们通过KVO来监听age值的改变
/*************** RevanPerson.h ***************/
#import <Foundation/Foundation.h>
@interface RevanPerson : NSObject
@property (nonatomic, assign) NSInteger age;
@end
/*************** RevanPerson.m ***************/
#import "RevanPerson.h"
@implementation RevanPerson
@end
/************* KVO测试代码 ***************/
#import "ViewController.h"
#import "RevanPerson.h"
@interface ViewController ()
@property (nonatomic, strong) RevanPerson *revanP1;
@end
@implementation ViewController
- (void)dealloc {
[self.revanP1 removeObserver:self forKeyPath:@"age"];
}
- (void)viewDidLoad {
[super viewDidLoad];
//revanP1
self.revanP1 = [[RevanPerson alloc] init];
self.revanP1.age = 11;
//策略
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld;
//给self.revanP1添加一个监听者:self
[self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.revanP1.age = 12;
}
#pragma mark - KVO的监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
,keyPath
,object
,change);
}
@end
当点击屏幕会触发touchesBegan:withEvent:方法来修改age的大小,由于age值的改变会触发KVO的
监听方法observeValueForKeyPath:ofObject:change:
输出如下:
keyPath=age
object=<RevanPerson: 0x60400000fa20>
change={
kind = 1;
new = 12;//age改变后的新值
old = 11; //age改变前的旧值
}
三、KVO原理分析
1、为了分析KVO原理,重新创建一个revanP2实例,但是不给revanP2实例添加KVO
/*************** RevanPerson.h ***************/
#import <Foundation/Foundation.h>
@interface RevanPerson : NSObject
@property (nonatomic, assign) NSInteger age;
@end
/*************** RevanPerson.m ***************/
#import "RevanPerson.h"
@implementation RevanPerson
@end
/************* KVO测试代码 ***************/
#import "ViewController.h"
#import "RevanPerson.h"
@interface ViewController ()
@property (nonatomic, strong) RevanPerson *revanP1;
@property (nonatomic, strong) RevanPerson *revanP2;
@end
@implementation ViewController
- (void)dealloc {
[self.revanP1 removeObserver:self forKeyPath:@"age"];
}
- (void)viewDidLoad {
[super viewDidLoad];
//revanP1
self.revanP1 = [[RevanPerson alloc] init];
self.revanP1.age = 11;
self.revanP2 = [[RevanPerson alloc] init];
self.revanP2.age = 21;
//策略
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld;
//给self.revanP1添加一个监听者:self
[self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.revanP1.age = 12;
self.revanP2.age = 22;
}
#pragma mark - KVO的监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
,keyPath
,object
,change);
}
打印输出:只有revanP1的输出
keyPath=age
object=<RevanPerson: 0x600000014cb0>
change={
kind = 1;
new = 12;
old = 11;
}
在触发touchesBegan:withEvent:方法时会给revanP1实例对象和revanP2实例对象的age赋值,会触发age的setAge:方法。Objective-C对象的分类中说过,对象方法信息是存储在class对象中的,类方法信息是存储在meta-class对象中的。所以我们先查看一下revanP1、revanP2的class对象
/************* KVO测试代码 ***************/
#import "ViewController.h"
#import "RevanPerson.h"
#import <objc/runtime.h>
@interface ViewController ()
@property (nonatomic, strong) RevanPerson *revanP1;
@property (nonatomic, strong) RevanPerson *revanP2;
@end
@implementation ViewController
- (void)dealloc {
[self.revanP1 removeObserver:self forKeyPath:@"age"];
}
- (void)viewDidLoad {
[super viewDidLoad];
//revanP1
self.revanP1 = [[RevanPerson alloc] init];
self.revanP1.age = 11;
self.revanP2 = [[RevanPerson alloc] init];
self.revanP2.age = 21;
Class revanP1_class = object_getClass(self.revanP1);
Class revanP2_class = object_getClass(self.revanP2);
NSLog(@"\nrevanP1KVO之前的class对象:%p-%@", revanP1_class, revanP1_class);
NSLog(@"\nrevanP2的class对象:%p-%@", revanP2_class, revanP2_class);
//策略
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld;
//给self.revanP1添加一个监听者:self
[self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
Class revanP1_class_kvo = object_getClass(self.revanP1);
NSLog(@"\nrevanP1KVO之后的class对象:%p-%@", revanP1_class_kvo, revanP1_class_kvo);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.revanP1.age = 12;
self.revanP2.age = 22;
}
#pragma mark - KVO的监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
,keyPath
,object
,change);
}
@end
打印输出:
revanP1KVO之前的class对象:0x10b0dc0b8-RevanPerson
revanP2的class对象:0x10b0dc0b8-RevanPerson
revanP1KVO之后的class对象:0x604000116140-NSKVONotifying_RevanPerson
- 在revanP1添加KVO之前,revanP1实例对象和revanP2实例对象的class对象都是是RevanPerson,在revanP1添加了KVO之后revan_P1实例对象的class对象是NSKVONotifying_RevanPerson。
- 既然revanP1对象的class对象是NSKVONotifying_RevanPerson,那为什么在给revanP1对象的age重新赋值的时候还可以改变age的值呢?NSKVONotifying_RevanPerson中有age这个属性?我们猜测NSKVONotifying_RevanPerson的父类是RevanPerson类
/************* KVO测试代码 ***************/
#import "ViewController.h"
#import "RevanPerson.h"
#import <objc/runtime.h>
@interface ViewController ()
@property (nonatomic, strong) RevanPerson *revanP1;
@property (nonatomic, strong) RevanPerson *revanP2;
@end
@implementation ViewController
- (void)dealloc {
[self.revanP1 removeObserver:self forKeyPath:@"age"];
}
- (void)viewDidLoad {
[super viewDidLoad];
//revanP1
self.revanP1 = [[RevanPerson alloc] init];
self.revanP1.age = 11;
self.revanP2 = [[RevanPerson alloc] init];
self.revanP2.age = 21;
Class revanP1_class = object_getClass(self.revanP1);
Class revanP2_class = object_getClass(self.revanP2);
NSLog(@"\nrevanP1KVO之前的class对象:%p-%@", revanP1_class, revanP1_class);
NSLog(@"\nrevanP2的class对象:%p-%@", revanP2_class, revanP2_class);
//策略
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld;
//给self.revanP1添加一个监听者:self
[self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
Class revanP1_class_kvo = object_getClass(self.revanP1);
NSLog(@"\nrevanP1KVO之后的class对象:%p-%@", revanP1_class_kvo, revanP1_class_kvo);
Class revanP1_superclass_kvo = [revanP1_class_kvo superclass];
NSLog(@"\nrevanP1KVO之后的superclass对象:%p-%@", revanP1_superclass_kvo, revanP1_superclass_kvo);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.revanP1.age = 12;
self.revanP2.age = 22;
}
#pragma mark - KVO的监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
,keyPath
,object
,change);
}
@end
打印输出:
revanP1KVO之前的class对象:0x1074dd0e0-RevanPerson
revanP2的class对象:0x1074dd0e0-RevanPerson
revanP1KVO之后的class对象:0x60000011c170-NSKVONotifying_RevanPerson
revanP1KVO之后的superclass对象:0x1074dd0e0-RevanPerson
- 通过代码测试后我们发现NSKVONotifying_RevanPerson这个类是RevanPerson类的子类。
2、NSKVONotifying_RevanPerson分析
在revanP1添加KVO的时候,系统会通过runtime动态的创建一个NSKVONotifying_RevanPerson类,并且把revanP1的isa指针指向NSKVONotifying_RevanPerson的class对象。当在给age赋值的时候其实是调用了age的setAge:方法,revanP2会直接调用RevanPerson中的setAge:方法,假设NSKVONotifying_RevanPerson类中没有重写setAge:方法,那么给revanP1.age赋值和给revanP2.age赋值的效果是一样的了,但是实际的情况是没有添加KVO的revanP2在给age赋值的时候是不会触发observeValueForKeyPath:ofObject:change:监听方法,所以猜测NSKVONotifying_RevanPerson类中重写了age的setAge:方法
/************* KVO测试代码 ***************/
#import "ViewController.h"
#import "RevanPerson.h"
#import <objc/runtime.h>
@interface ViewController ()
@property (nonatomic, strong) RevanPerson *revanP1;
@property (nonatomic, strong) RevanPerson *revanP2;
@end
@implementation ViewController
- (void)dealloc {
[self.revanP1 removeObserver:self forKeyPath:@"age"];
}
- (void)viewDidLoad {
[super viewDidLoad];
//revanP1
self.revanP1 = [[RevanPerson alloc] init];
self.revanP1.age = 11;
self.revanP2 = [[RevanPerson alloc] init];
self.revanP2.age = 21;
Class revanP1_class = object_getClass(self.revanP1);
Class revanP2_class = object_getClass(self.revanP2);
IMP revanP1_imp = [self.revanP1 methodForSelector:@selector(setAge:)];
IMP revanP2_imp = [self.revanP1 methodForSelector:@selector(setAge:)];
// NSLog(@"\nrevanP1KVO之前的class对象:%p-%@", revanP1_class, revanP1_class);
// NSLog(@"\nrevanP2的class对象:%p-%@", revanP2_class, revanP2_class);
NSLog(@"\nrevanP1KVO之前setAge:地址:%p", revanP1_imp);
NSLog(@"\nrevanP2的setAge:地址:%p", revanP2_imp);
//策略
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld;
//给self.revanP1添加一个监听者:self
[self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
Class revanP1_class_kvo = object_getClass(self.revanP1);
Class revanP1_superclass_kvo = [revanP1_class_kvo superclass];
IMP revanP1_imp_kvo = [self.revanP1 methodForSelector:@selector(setAge:)];
// NSLog(@"\nrevanP1KVO之后的class对象:%p-%@", revanP1_class_kvo, revanP1_class_kvo);
// NSLog(@"\nrevanP1KVO之后的superclass对象:%p-%@", revanP1_superclass_kvo, revanP1_superclass_kvo);
NSLog(@"\nrevanP1KVO之后setAge:地址:%p", revanP1_imp_kvo);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.revanP1.age = 12;
self.revanP2.age = 22;
}
#pragma mark - KVO的监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
,keyPath
,object
,change);
}
@end
打印输出:
revanP1KVO之前setAge:地址:0x1021317c0
revanP2的setAge:地址:0x1021317c0
revanP1KVO之后setAge:地址:0x102477bf4
- 在revanP1添加KVO之前,revanP1和revanP2在给age赋值的时候调用的方法是同一个方法。在revanP1添加KVO之后,revanP1在给age赋值时调用了另一个方法。这就说明在NSKVONotifying_RevanPerson中重写了父类的setAge:方法
-
查看revanP1和revanP2在给age赋值的时候各自调用了什么方法。revanP2调用了setAge:方法而revanP1调用了Foundation中的_NSSetLongLongValueAndNotify函数
查看调用的方法.png
- 查看对象方法
/*************** RevanPerson.h ***************/
#import <Foundation/Foundation.h>
@interface RevanPerson : NSObject
@property (nonatomic, assign) NSInteger age;
@end
/*************** RevanPerson.m ***************/
#import "RevanPerson.h"
@implementation RevanPerson
@end
/************* KVO测试代码 ***************/
#import "ViewController.h"
#import "RevanPerson.h"
#import <objc/runtime.h>
@interface ViewController ()
@property (nonatomic, strong) RevanPerson *revanP1;
@property (nonatomic, strong) RevanPerson *revanP2;
@end
@implementation ViewController
- (void)dealloc {
[self.revanP1 removeObserver:self forKeyPath:@"age"];
}
- (void)viewDidLoad {
[super viewDidLoad];
//revanP1
self.revanP1 = [[RevanPerson alloc] init];
self.revanP1.age = 11;
self.revanP2 = [[RevanPerson alloc] init];
self.revanP2.age = 21;
Class revanP1_class = object_getClass(self.revanP1);
Class revanP2_class = object_getClass(self.revanP2);
NSLog(@"KVO之前revanP1,class对象中对象方法信息");
[self methodNamesOfClass:revanP1_class];
NSLog(@"KVO之前revanP2,class对象中对象方法信息");
[self methodNamesOfClass:revanP2_class];
//策略
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld;
//给self.revanP1添加一个监听者:self
[self.revanP1 addObserver:self forKeyPath:@"age" options:options context:nil];
Class revanP1_class_kvo = object_getClass(self.revanP1);
NSLog(@"KVO之后revanP1,class对象中对象方法信息");
[self methodNamesOfClass:revanP1_class_kvo];
NSLog(@"KVO之后revanP2,class对象中对象方法信息");
[self methodNamesOfClass:revanP2_class];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.revanP1.age = 12;
self.revanP2.age = 22;
}
#pragma mark - KVO的监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"\nkeyPath=%@\nobject=%@\nchange=%@\n"
,keyPath
,object
,change);
}
- (void)methodNamesOfClass:(Class)cls
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
@end
打印输出:
KVO之前revanP1,class对象中对象方法信息:
RevanPerson setAge:, age,
KVO之前revanP2,class对象中对象方法信息:
RevanPerson setAge:, age,
KVO之后revanP1,class对象中对象方法信息:
NSKVONotifying_RevanPerson setAge:, class, dealloc, _isKVOA,
KVO之后revanP2,class对象中对象方法信息:
RevanPerson setAge:, age,
-
revanP2没有添加KVO
RevanPersoninstance对象.png
-
revanP1添加KVO
KVO底层原理.png
四、小结
- KVO本质
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指针指向这个全新的子类
- 当修改instance对象的属性时,全新的子类会重写属性的set方法,会调用Foundation的_NSSetXXXValueAndNotify函数
- willChangeValueForKey:
- 父类原来的setter方法
- didChangeValueForKey:
- 内部会触发监听器的监听方法observeValueForKeyPath:ofObject:change:context:
- 如何手动触发KVO
- 手动调用willChangeValueForKey:和didChangeValueForKey:
- 一个属性能不能使用KVO来监听,取决于是否可以重写该属性的set方法
网友评论