KVO全程就是“key-value observing”,俗称“键值监听”,用于对象属性值改变的监听。
上代码
#import "ViewController.h"
#import "MJPerson.h"
@interface ViewController ()
@property(nonatomic, strong) MJPerson *person1;
@property(nonatomic, strong) MJPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person2 = [[MJPerson alloc] init];
self.person1.age = 2;
self.person2.age = 3;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 6;
self.person2.age = 7;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end
点击屏幕改变person1
和person2
的age
的值,但是只对person1
的age
做了KVO
,运行起来看打印结果
打印看出只有person1
的age
的值改变有打印,但是点击屏幕改变了person1
和person2
的age
的值,那猜测是不是age
的set
方法有不同,重写MJPerson
的setAge:
方法。
#import "MJPerson.h"
@implementation MJPerson
- (void)setAge:(int)age
{
_age = age;
NSLog(@"newAge == %i", _age);
}
@end
打印结果
打印看出,点击屏幕改变person1
和person2
的age
的值,都走了MJPerson
的setAge:
方法,也就是在setAge:
方法上没有什么不同。
现在我们猜测是不是person1
和person2
的instance
对象的不同?
在person1
的age
做了KVO
前后分别打印person1
和person2
的class
对象
#import "ViewController.h"
#import "MJPerson.h"
#import <objc/runtime.h>
@interface ViewController ()
@property(nonatomic, strong) MJPerson *person1;
@property(nonatomic, strong) MJPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person2 = [[MJPerson alloc] init];
self.person1.age = 2;
self.person2.age = 3;
NSLog(@"监听前-----person1Class------%@", object_getClass(self.person1));
NSLog(@"监听前-----person2Class------%@", object_getClass(self.person2));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"监听前-----person1Class------%@", object_getClass(self.person1));
NSLog(@"监听前-----person2Class------%@", object_getClass(self.person2));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 6;
self.person2.age = 7;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end
打印结果
打印看出在person1
的age
实现KVO
后,person1
的Class
对象改变为NSKVONotifying_MJPerson
。也就是KVO
时,利用RuntimeAPI
动态生成一个NSKVONotifying_MJPerson
的类,并且让person1
对象的isa
指向这个全新的class
对象。
接下来探索NSKVONotifying_MJPerson
的class
对象有哪些方法实现
#import "ViewController.h"
#import "MJPerson.h"
#import <objc/runtime.h>
@interface ViewController ()
@property(nonatomic, strong) MJPerson *person1;
@end
@implementation ViewController
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 获取方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString new];
// 遍历所有方法
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);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 2;
[self printMethodNamesOfClass:object_getClass(self.person1)];
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self printMethodNamesOfClass:object_getClass(self.person1)];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 6;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end
打印结果
看打印结果可以看出变化如图
KVO 前 KVO 后通过打印可以看出,KVO
的实现监听主要NSKVONotifying_MJPerson
的class
对象的setAge:
方法里,可以用methodForSelector:
来打印查看setAge:
的实现
#import "ViewController.h"
#import "MJPerson.h"
#import <objc/runtime.h>
@interface ViewController ()
@property(nonatomic, strong) MJPerson *person1;
@end
@implementation ViewController
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 获取方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString new];
// 遍历所有方法
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);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 2;
// [self printMethodNamesOfClass:object_getClass(self.person1)];
NSLog(@"KVO前----%p", [self.person1 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
// [self printMethodNamesOfClass:object_getClass(self.person1)];
NSLog(@"KVO后----%p", [self.person1 methodForSelector:@selector(setAge:)]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 6;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end
断点打印
断点打印可以看到KVO
后NSKVONotifying_MJPerson
的class
对象的setAge:
变成了(Foundation _NSSetIntValueAndNotify)
。Foundation
框架的_NSSetIntValueAndNotify
函数,因为苹果Foundation
没开源,只能用逆向研究。
抽取后找到Foundation
的编译文件,用工具hopper反编译Foundation
的编译文件
看到不光有_NSSetIntValueAndNotify
,还有其他类型,猜测一下把age
换成 double
类型是不是NSKVONotifying_MJPerson
的setAge:
实现相应变为_NSSetDoubleValueAndNotify
?
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@property(nonatomic, assign) double age;
@end
断点打印
打印看出把age
换成 double
类型后,NSKVONotifying_MJPerson
的setAge:
实现确实相应变为_NSSetDoubleValueAndNotify
。
研究NSKVONotifying_MJPerson
和MJPerson
的关系
通过《isa、superclass的细节》可知,通过结构体mj_objc_class
来强制转化class
类
#import "ViewController.h"
#import "MJPerson.h"
#import <objc/runtime.h>
struct mj_objc_class {
Class isa;
Class superclass;
};
@interface ViewController ()
@property(nonatomic, strong) MJPerson *person1;
@end
@implementation ViewController
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 获取方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString new];
// 遍历所有方法
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);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 2;
// [self printMethodNamesOfClass:object_getClass(self.person1)];
// NSLog(@"KVO前----%p", [self.person1 methodForSelector:@selector(setAge:)]);
// 强制转换 KVO 前 person1 的 class 对象
struct mj_objc_class *person1Class = (__bridge struct mj_objc_class *)(object_getClass(self.person1));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
// [self printMethodNamesOfClass:object_getClass(self.person1)];
// NSLog(@"KVO后----%p", [self.person1 methodForSelector:@selector(setAge:)]);
// 强制转换 KVO 后 person1 的 class 对象
struct mj_objc_class *person1KVOClass = (__bridge struct mj_objc_class *)(object_getClass(self.person1));
NSLog(@"person1Class-------------%p", person1Class);
NSLog(@"person1KVOClass-------------%p", person1KVOClass);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 6;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end
断点打印
断点打印打印看到MJPerson
和NSKVONotifying_MJPerson
的class
对象不一样。
打印看到MJPerson
和NSKVONotifying_MJPerson
的meta-class
对象不一样。
打印看到NSKVONotifying_MJPerson
的父类是MJPerson
,MJPerson
的父类是NSObject
。
根据上面我们可以得到这个逻辑图
`MJPerson`和`NSKVONotifying_MJPerson`关系图现在来研究NSKVONotifying_MJPerson
的setAge:
内部的实现
通过hopper能看到_NSSetIntValueAndNotify
的实现,下面写一下伪代码
#import "NSKVONotifying_MJPerson.h"
@implementation NSKVONotifying_MJPerson
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age]
[self didChangeValueForKey:@"age"]
}
- (void)didChangeValueForKey:(NSString *)key
{
[observer observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
自定义NSKVONotifying_MJPerson
,KVO 的时候会失败报错,所以我们在父类MJPerson
中重写方法
#import "MJPerson.h"
@implementation MJPerson
- (void)setAge:(int)age
{
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key
{
NSLog(@"willChangeValueForKey:-----begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey:-----end");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey:-----begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey:-----end");
}
@end
#import "ViewController.h"
#import "MJPerson.h"
#import <objc/runtime.h>
struct mj_objc_class {
Class isa;
Class superclass;
};
@interface ViewController ()
@property(nonatomic, strong) MJPerson *person1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 2;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 6;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end
点击屏幕看打印顺序
点击屏幕打印 `_NSSet*ValueAndNotify`内部实现打印顺序可以看出_NSSetXXXValueAndNotify
的内部实现
- 调用
willChangeValueForKey:
- 调用原来的setter实现
- 调用
didChangeValueForKey:
内部会调用
observer
的observeValueForKeyPath:ofObject:change:context:
方法
面试题
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 利用
RuntimeAPi
动态生成一个NSKVONotifying_XXX
子类,并且让instance
对象的isa
指向NSKVONotifying_XXX
。 - 当调用instance对象的
set
方法时,会调用foundation框架的_NSSetXXXValueAndNotify
函数willChangeValueForKey:
- 父类的
set:
方法 -
didChangeValueForKey:
内部会调用监控器(Observer)的监听方法(
observeValueForKeyPath:ofObject:change:context:
)
如何手动触发KVO?
手动调用willChangeValueForKey:
和didChangeValueForKey:
直接修改成员变量会触发KVO么?
不会触发KVO(没有触发set:
方法)
网友评论