KVO:(Key-Value-Observering)键值监听,用于监听某个对象属性的变化
KVO的基本使用
#import "ViewController.h"
#import "MJPerson.h"
@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end
@implementation ViewController
- (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);
}
打印pesrson实例的isa指针,会发现添加监听的实例对象,不指向MJPerson类对象,而是指向NSKVONotifying_MJPerson,NSKVONotifying_MJPerson是属于MJPerson的子类。
(lldb) p self.person1 -> isa
(Class) $2 = NSKVONotifying_MJPerson
(lldb) p self.person2 -> isa
(Class) $3 = MJPerson
NSKVONotifying_MJPerson实现伪代码, set方法调用Foundation的c代码_NSSetIntValueAndNotify()函数
- (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];
}
使用KVO和未使用KVO监听的对象的区别
- NSKVONotifying_MJPerson和MJPerson拥有对象方法:age的set和get方法
- NSKVONotifying_MJPerson的superclass指针指向MJPerson,其class方法返回MJPerson类对象
观察对象使用KVO后变化
isa指向类对象
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));
NSLog(@"person1添加KVO监听之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
打印结果:
2020-05-06 22:29:04.845450+0800 Interview01[82395:1472694] person1添加KVO监听之前 - MJPerson MJPerson
2020-05-06 22:29:04.845547+0800 Interview01[82395:1472694] person1添加KVO监听之前 - 0x10c5864d0 0x10c5864d0
2020-05-06 22:29:04.845854+0800 Interview01[82395:1472694] person1添加KVO监听之后 - NSKVONotifying_MJPerson MJPerson
2020-05-06 22:29:04.845975+0800 Interview01[82395:1472694] person1添加KVO监听之后 - 0x7fff25623f0e 0x10c5864d0
设置KVO之后person1的类对象变成了NSKVONotifying_MJPerson,调用的方法打印值也不一样了。
lldb调试打印方法地址值, 这里的KVO方法发现其实调用的Foundation框架的_NSSetIntValueAndNotify方法(这里的不一定是_NSSetIntValueAndNotify,可能是_NSSetDoubleValueAndNotify等)
(lldb) p (IMP)0x7fff25623f0e
(IMP) $1 = 0x00007fff25623f0e (Foundation`_NSSetIntValueAndNotify)
(lldb) p (IMP)0x10b380510
(IMP) $2 = 0x000000010b380510 (Interview01`-[MJPerson setAge:] at MJPerson.m:13)
类对象的方法解析
#import <objc/runtime.h>
#import "MJPerson.h"
- (void)printMethodNamesOfClass:(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);
}
- (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)];
}
打印结果
2020-05-08 08:14:12.932361+0800 Interview01[91269:1963035] NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
2020-05-08 08:14:12.932498+0800 Interview01[91269:1963035] MJPerson age, setAge:,
NSKVONotifying_MJPerson返回4个对象方法
// 伪代码如下
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 屏幕内部实现,隐藏了NSKVONotifying_MJPerson类的存在
- (Class)class
{
return [MJPerson class];
}
- (void)dealloc
{
// 收尾工作
}
- (BOOL)_isKVOA
{
return YES;
}
其中重写class方法实现可以通过代码观察到
NSLog(@"person1:%@, person2:%@", object_getClass(self.person1), object_getClass(self.person2));
NSLog(@"person1:%@, person2:%@", [self.person1 class], [self.person1 class]);
打印值
2020-05-08 08:24:29.546611+0800 Interview01[91368:1969399] person1:NSKVONotifying_MJPerson, person2:MJPerson
2020-05-08 08:24:29.546762+0800 Interview01[91368:1969399] person1:MJPerson, person2:MJPerson
所以推测出这里class实现是屏幕内部实现,隐藏了NSKVONotifying_MJPerson类的存在。
总结
iOS利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类。当修改对象instance属性时,会调用Foundation的_NSSetXXXValueAndNotify函数。伪代码实现
- willChangeValueForKey
- 父类的Set方法
- didChangeValueForKey
内部触发监听器(Oberser)的监听方法(observerValueForKeyPath: ofObject:change:context)
网友评论