简单使用
KVO的全称是“Key-Value Observing”,俗称“健值监听”,可以用于监听某个对象属性值的改变,本质就是增加一个观察者,属性值改变的时候调用方法,告诉观察者。
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person1.height = 11;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
self.person2.height = 22;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self.person1 addObserver:self forKeyPath:@"height" options:options context:@"456"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 20;
self.person2.age = 20;
self.person1.height = 30;
self.person2.height = 30;
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
[self.person1 removeObserver:self forKeyPath:@"height"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
注意:KVO最后要在dealloc里移除。
KVO本质
KVO本质是利用runtime运行时生成了一个MJPerson的子类:NSKVONotifying_MJPerson类
这个子类复写了setAge实现,setAage实现调用了Fondation框架的 _NSSetXXValueAndNotify方法(_NSSetIntValueAndNotify、_NSSetCharValueAndNotify、_NSSetObjectValueAndNotify、_NSSetDoubleValueAndNotify)。
MJPerson的instance对象的isa指针指向NSKVONotifying_MJPerson的class对象,当我们调用MJPerson的setter方法时,就会通过isa指针找到NSKgVONotifying_MJPerson的setAge:方法。
_NSSetXXValueAndNotify又调用了以下几个方法:
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
- willChangeValueForKey、didChangeValueForKey.NSKVONotifying_MJPerson类没有这两个方法,是基类NSObject的方法。
[super setAge:age];调用父类的setter方法,也就是MJPerson的setAge:- didChangeValueForKey又调用了
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
伪代码如下:
#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
{
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
怎样验证
- 证明确实生成了NSKVONotifying_MJPerson类。1.我们自己创建个NSKVONotifying_MJPerson后KVO就会失败。 2.打印 self.person1.isa指针。
#import "ViewController.h"
#import "MJPerson.h"
#import <objc/runtime.h>
@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end
// 反编译工具 - Hopper
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// NSLog(@"person1添加KVO监听之前 - %@ %@",
// object_getClass(self.person1),
// object_getClass(self.person2));
NSLog(@"person1添加KVO监听之前 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
// NSLog(@"person1添加KVO监听之后 - %@ %@",
// object_getClass(self.person1),
// object_getClass(self.person2)); //object_getClass(self.person1)打印结果是NSKVONotifying_MJPerson,是通过isa指针找到类对象。
//methodForSelector方法打印方法的具体实现地址
NSLog(@"person1添加KVO监听之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
NSLog(@"类对象 - %@ %@",
object_getClass(self.person1), // self.person1.isa
object_getClass(self.person2)); // self.person2.isa
NSLog(@"元类对象 - %@ %@",
object_getClass(object_getClass(self.person1)), // self.person1.isa.isa
object_getClass(object_getClass(self.person2))); // self.person2.isa.isa
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// NSKVONotifying_MJPerson是使用Runtime动态创建的一个类,是MJPerson的子类
// self.person1.isa == NSKVONotifying_MJPerson
[self.person1 setAge:21];
// self.person2.isa = MJPerson
// [self.person2 setAge:22];
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end
-
KVO后打印
object_getClass(self.person1)发现是NSKVONotifying_MJPerson类对象。(说明object_getClass是通过isa指针找的) -
NSLog(@"元类对象 - %@ %@", object_getClass(object_getClass(self.person1)),可以发现NSKVONotifying_MJPerson的isa指针指向的是NSKVONotifying_MJPerson的元类对象,而不是MJPerson的元类对象。 -
NSLog(@"person1添加KVO监听之后 - %p %p", [self.person1 methodForSelector:@selector(setAge:)],这个方法打印方法的实现地址。然后通过p (IMP)地址值发现NSKVONotifying_MJPerson的setAge:实现是_NSXXXSetIntValueAndNotify.
总结:NSKVONotifying_MJPerson继承自MJPerson.MJPerson的instance对象的isa指针指向NSKVONotifying_MJPerson的类对象,NSKVONotifying_MJPerson类对象的isa指针指向自己的元类对象,superClass指针指向父类也就是MJPerson的类对象。
- 验证_NSXXXSetIntValueAndNotify调用的方法
#import "MJPerson.h"
@implementation MJPerson
- (void)setAge:(int)age
{
_age = age;
NSLog(@"setAge:");
}
//- (int)age
//{
// return _age;
//}
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
说明:我们覆写了willChangeValueForKey又调用了[super willChangeValueForKey].相当于没写这个函数。
通过打印结果可知确实如上所说。
窥探Foundation
可以用逆向方法,Hooper从手机共享动态库里提取出Foundation动态库,再用Hooper看。
或者 用nm.nm用于llvm symbol table dumper 导出符号表
nm Foundation | grep ValueAndNotify
子类NSKVONotifying_MJPerson的内部方法
@implementation ViewController
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 获得方法数组 runtime方法
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);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self.person1 setAge:21];
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// observeValueForKeyPath:ofObject:change:context:
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end
-
class_copyMethodList(<#Class _Nullable __unsafe_unretained cls#>, unsigned int * _Nullable outCount)这是个runtime方法,第二个参数放的是指针的值,所以传入&count -
Method *methodList = class_copyMethodList(cls, &count);C语言知识,可以用指针指向数组。 -
free(methodList);前面create或者copy了都要释放。
总结:通过打印得,NSKVONotifying_MJPerson有 setAge:, class, dealloc, _isKVOA方法
伪代码如下:
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 屏幕内部实现,隐藏了NSKVONotifying_MJPerson类的存在
- (Class)class
{
return [MJPerson class];
}
- (void)dealloc
{
// 收尾工作
}
- (BOOL)_isKVOA
{
return YES;
}
为什么会有class方法呢?
调[MJPerson class]发现不是对象isa指针指向的NSKVONotifying_MJPerson的类对象。因为NSKVONotifying_MJPerson对使用者是不可见的,所以要改写class方法。这也符合面对对象思想。
面试题
1.iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数。 1.willchangeValueForKey: 2.父类原来的setter 3.didChangeValueForKey:
- 内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
2.如何手动触发KVO ?
手动调用willchangeValueForKey:和didChangeValueForKey:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self.person1 setAge:21];
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
}
既然调用didChangeValueForKey会去调用通知观察者,为什么还要调用willChangeValueForKey?必须要调用,有可能didChangeValueForKey里面会判断是否调用willChangeValueForKey
- 直接修改成员变量会触发KVO么?
不会触发。