先来说说什么是KVO,KVO全称为Key Value Observing,键值监听机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,所以NSObject的子类都可使用该方法。
KVO的使用
1、注册观察者
//注册观察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
option:
-
NSKeyValueObservingOptionOld
把更改之前的值提供给处理方法 -
NSKeyValueObservingOptionNew
把更改之后的值提供给处理方法 -
NSKeyValueObservingOptionInitial
把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。 -
NSKeyValueObservingOptionPrior
分2次调用。在值改变之前和值改变之后。
context:
这里的context字面上面的意思是上下文。但是在实际的开发中,我们可以把它理解成为一个标记。通常是为了在类和类的子类中同时对一个属性进行监听时,为了区分两个监听,则可以在context中传入一个标识"person_name"
。如果不需要传入值,则传入NULL
即可。
那么用context有什么好处呢?在我们接收属性变化的回调的时候,同时会拿到相应的
keyPath
、object
、change
和context
。如果去判断keyPath
的话,我们需要判断缓存列表,还要判断类的列表,才能找到相应的属性值。但是context
可以看做是一个静态的值放在内存中,所以用context
会让性能提升不少。
2、监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@",change);
}
3、移除观察者
- (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.
- An 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.
- 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.
解除分配时,观察者不会自动删除自身。被观察对象继续发送通知,无视观察者的状态。但是发送到已发布对象的更改通知与任何其他消息一样,会去出发内存访问异常。因此,要确保观察者在从内存中消失之前将其移除。
KVO的监听
1、自动监听属性值
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return YES;
}
return NO;
}
2、手动观察属性值
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
KVO原理分析
在查看了官方文档之后,发现KVO的原理其实是改变了isa指针,将isa指针指向了另一个动态生成的类。为了分析其中的原理,我们在下图地方做一个断点,看看到底是生成了什么样的类。
image然后我们用LLDB打印一下相应的类。
image我们发现出来了一个新的类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的原理:
- 验证是否存在setter方法,目的是为了不让实例进来
- 动态生成子类NSKVONotifying_Person:先开辟一个新的类,然后注册类,重写class的方法,讲class指向Person,接着重写setter方法,通过对setter赋值,实现父类的方法self.name = @"xlh",最后通过objc_getAssociatedObject关联住我们的观察者
- 讲isa的指针指向NSKVONotifying_Person
- 最后通过消息转发响应响应的回调
网友评论