先来讲说什么是KVO, KVO全称为Key Value Observing,键值监听机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,因此NSObject的子类均可使用该方法。
一、注册观察者数组
//注册观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
复制代码
option:缓存
NSKeyValueObservingOptionOld
把更改以前的值提供给处理方法NSKeyValueObservingOptionNew
把更改以后的值提供给处理方法NSKeyValueObservingOptionInitial
把初始化的值提供给处理方法,一旦注册,立马就会调用一次。一般它会带有新值,而不会带有旧值。NSKeyValueObservingOptionPrior
分2次调用。在值改变以前和值改变以后。context:bash
这里的context字面上面的意思是上下文。可是在实际的开发中,咱们能够把它理解成为一个标记。一般是为了在类和类的子类中同时对一个属性进行监听时,为了区分两个监听,则能够在context中传入一个标识"person_name"
。若是不须要传入值,则传入NULL
便可。app
那么用context有什么好处呢?在咱们接收属性变化的回调的时候,同时会拿到相应的keyPath
、object
、change
和context
。若是去判断keyPath
的话,咱们须要判断缓存列表,还要判断类的列表,才能找到相应的属性值。可是context
能够看作是一个静态的值放在内存中,因此用context
会让性能提高很多。
二、监听回调ide
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@",change);
}复制代码
三、移除观察者函数
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}复制代码
那么咱们可能要思考为何要移除观察者呢?咱们再查看了官方文档以后,就能清楚的看见:性能
解除分配时,观察者不会自动删除自身。被观察对象继续发送通知,无视观察者的状态。可是发送到已发布对象的更改通知与任何其余消息同样,会去出发内存访问异常。所以,要确保观察者在从内存中消失以前将其移除。When removing an observer, keep several points in mind:测试
Asking to be removed as an observer if not already registered as one results in an
NSRangeException
. You either callremoveObserver:forKeyPath:context:
exactly once for the corresponding call toaddObserver:forKeyPath:options:context:
, or if that is not feasible in your app, place theremoveObserver:forKeyPath:context:
call inside a try/catch block to process the potential exception.uiAn observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.spa
The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in
init
orviewDidLoad
) and unregister during deallocation (usually indealloc
), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.
一、自动监听属性值
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return YES;
}
return NO;
}复制代码
二、手动观察属性值
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}复制代码
在查看了官方文档以后,发现KVO的原理实际上是改变了isa指针,将isa指针指向了另外一个动态生成的类。为了分析其中的原理,咱们在下图地方作一个断点,看看究竟是生成了什么样的类。
而后咱们用LLDB打印一下相应的类。
咱们发现出来了一个新的类NSKVONotifing_Person
。那么这个新的类跟Person类是什么关系呢?因而决定打印添加了observer先后类到底有什么变化。
调用printClasses方法能够打印全部的子类的信息:
#pragma mark - ======== 遍历类以及子类 ========
- (void)printClasses:(Class)cls {
//注册类的总和
int count = objc_getClassList(NULL, 0);
//建立一个数组,其中包含给定的对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject: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(@"classes = %@",mArray);
}复制代码
而后咱们再调用前和调用后分别调用方法:
[self printClasses:[Person class]];
NSLog(@"*********添加前*********");
//[self printClasses:NSClassFromString(@"NSKOVNotifing_Person")];
self.person = [[Person alloc]init];
self.person.name = @"zy";
//注册观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"*********进去了*********");
[self printClasses:[Person class]];
NSLog(@"*********添加后:NSKVONotifying_Person*********");
[self printClasses:NSClassFromString(@"NSKVONotifying_Person")];复制代码
打印出来的值为
2019-04-14 20:27:48.150739+0800 KVODemo[1959:134800] classes = (
Person
)
2019-04-14 20:27:48.150926+0800 KVODemo[1959:134800] *********添加前*********
2019-04-14 20:27:48.151333+0800 KVODemo[1959:134800] *********进去了*********
2019-04-14 20:27:48.157911+0800 KVODemo[1959:134800] classes = (
Person,
"NSKVONotifying_Person"
)
2019-04-14 20:27:48.158066+0800 KVODemo[1959:134800] *********添加后:NSKVONotifying_Person*********
2019-04-14 20:27:48.162250+0800 KVODemo[1959:134800] classes = (
"NSKVONotifying_Person"
)
那么咱们能够明确的看到,NSKVONotifying_Person是继承与Person的,他是动态生成的Person的子类。
弄清楚了类的关系,咱们再看看方法有什么变化。咱们新增一个遍历全部方法的函数:
#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);
}复制代码
而后在添加先后分别调用printClassAllMethod
方法,打印的结果为:
2019-04-14 20:44:43.451245+0800 KVODemo[2131:144186] hello-0x10a759290
2019-04-14 20:44:43.451427+0800 KVODemo[2131:144186] world-0x10a7592c0
2019-04-14 20:44:43.451529+0800 KVODemo[2131:144186] nick-0x10a759320
2019-04-14 20:44:43.451633+0800 KVODemo[2131:144186] setNick:-0x10a759350
2019-04-14 20:44:43.451738+0800 KVODemo[2131:144186] .cxx_destruct-0x10a759390
2019-04-14 20:44:43.451879+0800 KVODemo[2131:144186] name-0x10a7592f0
2019-04-14 20:44:43.451963+0800 KVODemo[2131:144186] setName:-0x10a7591f0
2019-04-14 20:44:43.452100+0800 KVODemo[2131:144186] *********添加前*********
2019-04-14 20:44:43.452582+0800 KVODemo[2131:144186] *********进去了*********
2019-04-14 20:44:43.452672+0800 KVODemo[2131:144186] *********添加后:NSKVONotifying_Person*********
2019-04-14 20:44:43.452786+0800 KVODemo[2131:144186] setName:-0x10aab263a
2019-04-14 20:44:43.452884+0800 KVODemo[2131:144186] class-0x10aab106e
2019-04-14 20:44:43.452968+0800 KVODemo[2131:144186] dealloc-0x10aab0e12
2019-04-14 20:44:43.453067+0800 KVODemo[2131:144186] _isKVOA-0x10aab0e0a
那么,经过对方法的地址分析,咱们能够获得一个结论,NSKVONotifying_Person
类重写了setName
的方法,而后新增了class
方法、dealloc
方法和_isKVOA
方法。
综合上面的测试,咱们能够总结出来KVO的原理: