前言
对于KVO
技术,开发中使用较多,能够监听值的改变。
探讨下KVO
的技术实现,KVO
接口如下:
@interface NSObject(NSKeyValueObserverRegistration)
/* 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.
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
有如下问题:
KVO
是如何实现监听属性值变化的addObserver
是添加观察者,那么它的引用计数会不会改变呢?
探索
1. 引用问题
有如下代码:
KVOObject *myobj = [[KVOObject alloc] init];
myobj.name = @"";
//注释部分1
// KVOObserver *observer = [[KVOObserver alloc] init];
// NSLog(@"before add %@",[observer valueForKey:@"retainCount"]);
// [myobj addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
// NSLog(@"after add %@",[observer valueForKey:@"retainCount"]);
// myobj.name = @"yanfang";
NSLog(@"before block %ld",(long)CFGetRetainCount((__bridge CFTypeRef)(myobj)));
dispatch_block_t block = ^{
NSLog(@"do action");
myobj.name = @"yanfang2";
NSLog(@"in block %ld",(long)CFGetRetainCount((__bridge CFTypeRef)(myobj)));
};
NSLog(@"after block %ld",(long)CFGetRetainCount((__bridge CFTypeRef)(myobj)));
NSLog(@"block %@",block);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), block);
执行并没有问题,myobj
会被block
捕获,引用计数+1
,block
执行完之后myobj
释放。
当我们将注释部分恢复时,就出现了崩溃EXC_BAD_ACCESS
,那么既然不是myobj
的问题,那么就是observer
提前释放了,而myobj
保持了他的_unsafe_unretain
的指针,也就不是weak
类型指针,导致了崩溃。这和NSNotifcationCenter
崩溃的原因应该一致。
2. KVO原理
我们通过addObserver
前后打印其isa指针
printf("before %s \n",object_getClassName(myobj));
[myobj addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
printf("after %s \n",object_getClassName(myobj));
输出如下:
before KVOObject
after NSKVONotifying_KVOObject
这里其实就使用了isa-swizzling
,原理是,将KVOObject
的isa
修改为NSKVONotifying_KVOObject
类型(NSKVONotifying_KVOObject
继承自KVOObject
),这样子runtime
进行消息发送时,首先检查的是isa
,通过isa
,判断其是NSKVONotifying_KVOObject
类型,就去NSKVONotifying_KVOObject
的方法列表中去找。这样就达到了将原类方法映射到新类上的目的。
hook框架Aspect也是利用了这一原理,进行了isa-swizzling
关于如何改变isa
指针,runtime
源码中有如下接口可以参考:
/***********************************************************************
* object_getClass.
* Locking: None. If you add locking, tell gdb (rdar://7516456).
**********************************************************************/
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
/***********************************************************************
* object_setClass.
**********************************************************************/
Class object_setClass(id obj, Class cls)
{
if (!obj) return nil;
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
// Unresolved future classes are not so protected.
if (!cls->isFuture() && !cls->isInitialized()) {
_class_initialize(_class_getNonMetaClass(cls, nil));
}
return obj->changeIsa(cls);
}
如何关闭KVO
重写automaticallyNotifiesObserversForKey
,这个方法会在调用addObserver:...
方法时调用
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"name"]) {
return NO;
}else{
return [super automaticallyNotifiesObserversForKey:key];
}
}
isa是如何恢复的
有没有想过修改其isa之后,怎么恢复呢?蒙圈了吧 = =,这里贴出 GNUStep
实现
@implementation NSObject (NSKeyValueObserverRegistration)
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
GSKVOInfo *info;
GSKVOReplacement *r;
NSKeyValueObservationForwarder *forwarder;
NSRange dot;
setup();
[kvoLock lock];
// Use the original class
r = replacementForClass([self class]);
/*
* Get the existing observation information, creating it (and changing
* the receiver to start key-value-observing by switching its class)
* if necessary.
*/
info = (GSKVOInfo*)[self observationInfo];
if (info == nil)
{
info = [[GSKVOInfo alloc] initWithInstance: self];
[self setObservationInfo: info];
object_setClass(self, [r replacement]);
}
/*
* Now add the observer.
*/
dot = [aPath rangeOfString:@"."];
if (dot.location != NSNotFound)
{
forwarder = [[NSKeyValueObservationForwarder alloc]
initWithKeyPath: aPath
ofObject: self
withTarget: anObserver
context: aContext];
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: forwarder];
}
else
{
[r overrideSetterFor: aPath];
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: aContext];
}
[kvoLock unlock];
}
- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
GSKVOInfo *info;
id forwarder;
/*
* Get the observation information and remove this observation.
*/
info = (GSKVOInfo*)[self observationInfo];
forwarder = [info contextForObserver: anObserver ofKeyPath: aPath];
[info removeObserver: anObserver forKeyPath: aPath];
if ([info isUnobserved] == YES)
{
/*
* The instance is no longer being observed ... so we can
* turn off key-value-observing for it.
*/
object_setClass(self, [self class]);
IF_NO_GC(AUTORELEASE(info);)
[self setObservationInfo: nil];
}
if ([aPath rangeOfString:@"."].location != NSNotFound)
[forwarder finalize];
}
@end
使用的 object_setClass(self, [self class]);
来还原,这为啥管用? KVO因为重写了其 class 方法,返回的是原来的类型
,让你没有感觉isa被替换掉,所以 [self class]
这里就没有问题。
Demo链接
链接:https://pan.baidu.com/s/1VZBzclWnK4w5_1Vrcd4TSQ
密码:md43
参考文章
https://www.jianshu.com/p/d509c78c59bd
https://www.jianshu.com/p/badf5cac0130
时时刻刻关注着她,往往输得很惨,所以慎用KVO哦