KVO 苹果文档地址
KVO: Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
键值编码是一种机制,允许在其他对象(被监听对象)的指定属性发生更改时被通知到监听对象。
一、KVO的简单使用
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor redColor];
self.person = [MyPerson new];
self.person.name = @"initName";
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"newkey=%@ - oldKey=%@",change[NSKeyValueChangeNewKey],change[NSKeyValueChangeOldKey]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@+",self.person.name];
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name" context:NULL];
}
方法注释:
`- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;`
/* Register or deregister as an observer of the value at a key path relative to the receiver.
The options determine what is included in observer notifications and when they're sent,
as described above, and the context is passed in observer notifications as described above.
You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible
because it allows you to more precisely specify your intent.
When the same observer is registered for the same key path multiple times,
but with different context pointers each time, -removeObserver:forKeyPath:
has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
1、context
- context
:上下文,它是什么呢?--> 通过苹果文档和实际场景均可得知,context
用来更安全精确便利 地指定和区分开我们的监听;
为何forKeyPath
来区分不好?--> 当存在监听多个对象/多个属性
,在处理监听时 仅仅判断监听的是哪个对象 就挺麻烦了,性能和代码可读性会变得复杂。
建议使用context
.
2、removeObserver - 移除观察者
Removing an Object as an Observer 苹果文档
1)移除观察者
-
observer
不会在deallocated
时自动移除; - 使用
-removeObserver:forKeyPath:context:
;
通过注释:你应该尽可能使用-removeObserver:forKeyPath:context:
而不是-removeObserver:forKeyPath:
因为它允许你更精确地指定你的意图。当同一个观察者为相同的键路径多次注册,但每次都使用不同的上下文指针时,-removeObserver:forKeyPath:必须在决定删除什么内容时猜测上下文指针,而且它可能猜错了。
2)不移除观察者会出现的问题
创建MySubPerson
单例代码如下:
@implementation MySubPerson
static MySubPerson* _instance = nil;
+ (instancetype)shareInstance{
static dispatch_once_t onceToken ;
dispatch_once(&onceToken, ^{
_instance = [[super allocWithZone:NULL] init] ;
}) ;
return _instance ;
}
//- (void)personInstanceOne {
// NSLog(@"%s",__func__);
//}
+ (void)personClassOne {
NSLog(@"%s",__func__);
}
@end
分别在2(MyController1)
、3(MyController2)
级页面为其添加观察者,2级页面dealloc
时不移除监听,运行工程:
报错如下:
原因分析:
1:
person
是个MySubPerson
单例对象,给它添加了2个监听,但是有一个未移除,person
对象一直还在内存中;2:再次进入页面又添加了一个监听,当属性改变时,需要给两个监听都发消息,但是只能找到现有的对象,原来的那个找不着了是个野指针了,它便导致崩溃。
3、开关 KVO
- 系统方法
MyPerson
类中将自动打开设为NO
(默认是YES):
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
再次运行工程,监听不到属性变化了。
- 手动打开
代码如下:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
再次运行工程,可正常监听到变化。
4、监听集合类型
以数组为例.
// 数组
self.person.mDataArray = [NSMutableArray array];
[self.person addObserver:self forKeyPath:@"mDataArray" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"newkey=%@ - oldKey=%@",change[NSKeyValueChangeNewKey],change[NSKeyValueChangeOldKey]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// self.person.name = [NSString stringWithFormat:@"%@-",self.person.name];
[self.person.mDataArray addObject:@"one"];
}
并未监听到变化,查看苹果文档所给信息:
KVO
是基于KVC
的,我们对代码进行如下修改:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@-",self.person.name];
[[self.person mutableArrayValueForKey:@"mDataArray"] addObject:@"one"]; //[self.person.mDataArray addObject:@"one"];
}
运行工程,监听结果:
2020-10-28 18:47:03.337414+0800 DemoEmpty_iOS[9449:394437] {
kind = 1;// NSKeyValueChangeSetting
new = "(null)-";
old = "<null>";
}
2020-10-28 18:47:03.338002+0800 DemoEmpty_iOS[9449:394437] {
indexes = "<_NSCachedIndexSet: 0x60000396e700>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;// NSKeyValueChangeInsertion
new = (
one
);
}
KVO
部分代码:
/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
二、 KVO
底层原理
1、属性和成员
MyController2
中主要代码如下:
self.person = [[MyPerson alloc]init];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
[self.person addObserver:self forKeyPath:@"ivarName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"赋值前 name=%@ - ivarName=%@",self.person.name,self.person->ivarName);
self.person.name = @"名字";
self.person->ivarName = @"昵称";
NSLog(@"赋值后 name=%@ / ivarName=%@",self.person.name,self.person->ivarName);
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"observer %@",change);
NSLog(@"newkey=%@ / oldKey=%@",change[NSKeyValueChangeNewKey],change[NSKeyValueChangeOldKey]);
}
运行工程,输出:
2020-10-28 23:07:35.530041+0800 DemoEmpty_iOS[9847:515045] 赋值前 name=(null) - ivarName=(null)
2020-10-28 23:07:35.530594+0800 DemoEmpty_iOS[9847:515045] observer {
kind = 1;
new = "\U540d\U5b57";
old = "<null>";
}
2020-10-28 23:07:35.530846+0800 DemoEmpty_iOS[9847:515045] newkey=名字 / oldKey=<null>
2020-10-28 23:07:35.531069+0800 DemoEmpty_iOS[9847:515045] 赋值后 name=名字 / ivarName=昵称
KVO
只对属性进行观察 --> setter
方法。
下面对KVO
原理进行探究。
2、KVO
原理
1)运行工程,查看self.person
的isa
:
self.person = [[MyPerson alloc]init];// MyPerson
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];// NSKVONotifying_MyPerson
/*
(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x000000010d02317a "MyPerson"
(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $2 = 0x00006000023768e0 "NSKVONotifying_MyPerson"
*/
添加KVO
监听后,self.person
的isa
指向由MyPerson
变成了一个中间类 - NSKVONotifying_MyPerson
.NSKVONotifying_MyPerson
是个什么类?
在监听前后分别输出打印下所有类子类 与 方法:
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls {
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];// 先把 cls 自己放进去
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"controller:classes = %@", mArray);
}
输出结果:
2020-10-28 23:30:45.280579+0800 DemoEmpty_iOS[9977:527858] controller:classes = (
MyPerson,
MySubPerson
)
2020-10-28 23:30:51.984501+0800 DemoEmpty_iOS[9977:527858] controller:classes = (
MyPerson,
"NSKVONotifying_MyPerson",
MySubPerson
)
NSKVONotifying_MyPerson
是MyPerson
的一个派生子类。
2)MyPerson
和NSKVONotifying_MyPerson
的方法
2.1、分别输出2者的全部方法
#pragma mark - 遍历方法 - ivar - property
- (void)printClassAllMethod:(Class)cls {
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
MyPerson
的所有方法:
2020-10-28 23:42:31.320759+0800 DemoEmpty_iOS[10015:533265] nick-0x10bc2aa50
2020-10-28 23:42:31.320985+0800 DemoEmpty_iOS[10015:533265] setNick:-0x10bc2aa80
2020-10-28 23:42:31.321095+0800 DemoEmpty_iOS[10015:533265] personInstanceOne-0x10bc2a980
2020-10-28 23:42:31.321274+0800 DemoEmpty_iOS[10015:533265] mDataArray-0x10bc2aac0
2020-10-28 23:42:31.321421+0800 DemoEmpty_iOS[10015:533265] setMDataArray:-0x10bc2aae0
2020-10-28 23:42:31.321563+0800 DemoEmpty_iOS[10015:533265] .cxx_destruct-0x10bc2ab60
2020-10-28 23:42:31.321688+0800 DemoEmpty_iOS[10015:533265] name-0x10bc2a9e0
2020-10-28 23:42:31.321800+0800 DemoEmpty_iOS[10015:533265] setName:-0x10bc2aa10
2020-10-28 23:42:31.321922+0800 DemoEmpty_iOS[10015:533265] age-0x10bc2ab20
2020-10-28 23:42:31.322032+0800 DemoEmpty_iOS[10015:533265] setAge:-0x10bc2ab40
NSKVONotifying_MyPerson
的所有方法:
2020-10-28 23:50:47.534742+0800 DemoEmpty_iOS[10055:538121] setName:-0x7fff25721c7a
2020-10-28 23:50:47.534892+0800 DemoEmpty_iOS[10055:538121] class-0x7fff2572073d
2020-10-28 23:50:47.534990+0800 DemoEmpty_iOS[10055:538121] dealloc-0x7fff257204a2
2020-10-28 23:50:47.535088+0800 DemoEmpty_iOS[10055:538121] _isKVOA-0x7fff2572049a
2.2、问题:NSKVONotifying_MyPerson
是继承还是重写呢?
新建继承自MyPerson
的类MySubPerson
,输出其全部方法
--> 没有任何输出!
--> 在MySubPerson
中实现任一方法(例:setNick:
),输出结果如下:
2020-10-29 00:08:21.050077+0800 DemoEmpty_iOS[10196:549729] setNick:-0x103515530
对比NSKVONotifying_MyPerson
,完全不同。
可知NSKVONotifying_MyPerson
的方法不是继承来的,而是自己重写的,且方法一定是有实现的。
3)setName
问题:setter
方法是如何重写了父类的属性的呢?
3.1、添加观察点
在addObserver
前断点,给观察的属性name
添加观察点 watchpoint set variable self->_person->_name
,添加成功:
(lldb) watchpoint set variable self->_person->_name
Watchpoint created: Watchpoint 1: addr = 0x60000377d630 size = 8 state = enabled type = w
watchpoint spec = 'self->_person->_name'
new value: 0x0000000000000000
放开断点,继续执行,当name
发生变化时,watchpoint 1 hit
:
Watchpoint 1 hit:
old value: 0x0000000000000000
new value: 0x000000010b493108
此时堆栈信息见下图:
查看2、3、4
的内容信息:
2
中信息如下:
更详细内容:
0x7fff257273ea <+457>: leaq -0x3a7e(%rip), %r8 ; NSKeyValueWillChangeBySetting
0x7fff257273f1 <+464>: movq -0x590(%rbp), %r9
0x7fff257273f8 <+471>: pushq $0x0
0x7fff257273fa <+473>: pushq %rbx
0x7fff257273fb <+474>: leaq 0x4da(%rip), %rax ; NSKeyValuePushPendingNotificationLocal
0x7fff25727402 <+481>: pushq %rax
0x7fff25727403 <+482>: callq 0x7fff257275d2 ; NSKeyValueWillChange
0x7fff25727408 <+487>: addq $0x20, %rsp
0x7fff2572740c <+491>: incq %r12
0x7fff2572740f <+494>: cmpq %r12, %r14
0x7fff25727412 <+497>: jne 0x7fff257273c6 ; <+421>
0x7fff25727414 <+499>: cmpq $0x0, -0x570(%rbp)
0x7fff2572741c <+507>: je 0x7fff2572747f ; <+606>
0x7fff2572741e <+509>: movb $0x0, -0x540(%rbp)
0x7fff25727425 <+516>: decq %r14
0x7fff25727428 <+519>: js 0x7fff2572747f ; <+606>
0x7fff2572742a <+521>: leaq -0x560(%rbp), %rbx
0x7fff25727431 <+528>: leaq -0x3ac5(%rip), %r12 ; NSKeyValueWillChangeBySetting
0x7fff25727438 <+535>: movq -0x598(%rbp), %rax
... 信息太多 省略一部分 ...
0x7fff25727469 <+584>: leaq 0x46c(%rip), %rax ; NSKeyValuePushPendingNotificationLocal
0x7fff25727470 <+591>: pushq %rax
0x7fff25727471 <+592>: callq 0x7fff257275d2 ; NSKeyValueWillChange
0x7fff25727476 <+597>: addq $0x20, %rsp
0x7fff2572747a <+601>: decq %r14
0x7fff2572747d <+604>: jns 0x7fff25727438 ; <+535>
0x7fff2572747f <+606>: movq -0x550(%rbp), %rax
0x7fff25727486 <+613>: movq %rax, -0x578(%rbp)
0x7fff2572748d <+620>: movq -0x548(%rbp), %r14
0x7fff25727494 <+627>: movq -0x588(%rbp), %rbx
0x7fff2572749b <+634>: movq 0x10(%rbp), %rdi
0x7fff2572749f <+638>: testq %rdi, %rdi
0x7fff257274a2 <+641>: je 0x7fff257274a7 ; <+646>
0x7fff257274a4 <+643>: callq *0x10(%rdi)
-> 0x7fff257274a7 <+646>: testq %r14, %r14
0x7fff257274aa <+649>: jle 0x7fff2572750a ; <+745>
0x7fff257274ac <+651>: leaq -0x560(%rbp), %rax
0x7fff257274b3 <+658>: movq -0x578(%rbp), %rcx
0x7fff257274ba <+665>: movq %rcx, (%rax)
0x7fff257274bd <+668>: movq %r14, 0x8(%rax)
0x7fff257274c1 <+672>: xorl %ecx, %ecx
0x7fff257274c3 <+674>: movq %rcx, 0x20(%rax)
0x7fff257274c7 <+678>: movq %rcx, 0x18(%rax)
0x7fff257274cb <+682>: movq %rcx, 0x10(%rax)
0x7fff257274cf <+686>: movq -0x568(%rbp), %rcx
0x7fff257274d6 <+693>: movq %rcx, 0x28(%rax)
0x7fff257274da <+697>: subq $0x8, %rsp
0x7fff257274de <+701>: leaq -0x39ff(%rip), %rcx ; NSKeyValueDidChangeBySetting
0x7fff257274e5 <+708>: leaq 0x77f(%rip), %r9 ; NSKeyValuePopPendingNotificationLocal
... 信息太多 省略一部分 ...
0x7fff25727501 <+736>: callq 0x7fff25727a10 ; NSKeyValueDidChange
0x7fff25727506 <+741>: addq $0x10, %rsp
0x7fff2572750a <+745>: movq -0x568(%rbp), %rdi
0x7fff25727511 <+752>: callq *0x5b23a951(%rip) ; (void *)0x00007fff51411000: objc_release
0x7fff25727517 <+758>: testq %r13, %r13
0x7fff2572751a <+761>: je 0x7fff25727534 ; <+787>
0x7fff2572751c <+763>: xorl %ebx, %ebx
0x7fff2572751e <+765>: movq 0x5b23a943(%rip), %r14 ; (void *)0x00007fff51411000: objc_release
0x7fff25727525 <+772>: movq (%r15,%rbx,8), %rdi
通过上面信息可知,断点在0x7fff257274a7 <+646>: testq %r14, %r14
处,在此前后流程,汇编代码流程中分别有 NSKeyValueWillChange
和NSKeyValueDidChange
,而NSKeyValueDidChange
前又走进了父类的setter
方法;
可得知 流程 -->NSKeyValueWillChange
--> 父类[MyPerson setName]
-->NSKeyValueDidChange
-->objc_release
这里即可验证父类的属性值为何会变化。
4)问:NSKVONotifying_MyPerson
会被移除吗?isa
是否会指回MyPerson
?
- (void)dealloc {
[self printClassAllMethod:objc_getClass("NSKVONotifying_MyPerson")];
[self.person removeObserver:self forKeyPath:@"name" context:NULL];
[self printClassAllMethod:objc_getClass("NSKVONotifying_MyPerson")];
}
析构函数中的removeObserver
前后依次输出信息如下:
2020-10-29 00:29:47.516214+0800 DemoEmpty_iOS[10221:558663] setName:-0x7fff25721c7a
2020-10-29 00:29:47.516397+0800 DemoEmpty_iOS[10221:558663] class-0x7fff2572073d
2020-10-29 00:29:47.516561+0800 DemoEmpty_iOS[10221:558663] dealloc-0x7fff257204a2
2020-10-29 00:29:47.516654+0800 DemoEmpty_iOS[10221:558663] _isKVOA-0x7fff2572049a
(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $0 = 0x000060000324bc00 "NSKVONotifying_MyPerson"
(lldb) p/x object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x000000010e1ca17c "MyPerson"
2020-10-29 00:30:03.485147+0800 DemoEmpty_iOS[10221:558663] setName:-0x7fff25721c7a
2020-10-29 00:30:03.485293+0800 DemoEmpty_iOS[10221:558663] class-0x7fff2572073d
2020-10-29 00:30:03.485441+0800 DemoEmpty_iOS[10221:558663] dealloc-0x7fff257204a2
2020-10-29 00:30:03.485540+0800 DemoEmpty_iOS[10221:558663] _isKVOA-0x7fff2572049a
(lldb)
返回到其他页面再次输出,NSKVONotifying_MyPerson
仍是存在的。
答:
由上可得知,在移除监听的观察者后,isa
指回了MyPerson
。而NSKVONotifying_MyPerson
会一直在内存中并不会移除。这里若下次再进来不必再次开辟加载,节省了时间性能。
KVO Demo 地址
总结:
-
KVO
的实现使用了isa-swizzing
; - 中间类
NSKVONotifying_clsName
是clsName
的子类,并重写了方法; -
NSKVONotifying_clsName
不会被销毁,一直存在在内存中,生命和普通类相同; - 析构
removeObserver
后,对象的isa
会指回clsName
。
三、自定义 KVO
模拟系统,自定义实现 KVO
思路:
- 验证是否存在
setter
方法,处理实例对象不监听 - 动态生成子类 -
NSKVONotifying_XXX
2.1 申请类
2.2 注册
2.3 添加方法 - isa 指向
- 父类 setter
- 观察者去响应
- removeObserver