下面的代码会在调用NSKVOUnionSetAndNotify
的{{1}}内部崩溃,其中包含的内容似乎是虚假字典。
似乎是混合的CFDictionaryGetValue
/ addFoos
代码与添加和删除KVO观察者的行为之间的竞争。
NSKVOUnionSetAndNotify
答案 0 :(得分:6)
当加入键值观察词典时,您获得的实际崩溃是EXC_BAD_ACCESS
。堆栈跟踪如下:
* thread #2: tid = 0x1ade39, 0x00007fff92f8e097 libobjc.A.dylib`objc_msgSend + 23, queue = 'com.apple.root.default-priority', stop reason = EXC_BAD_ACCESS (code=1, address=0x18)
frame #0: 0x00007fff92f8e097 libobjc.A.dylib`objc_msgSend + 23
frame #1: 0x00007fff8ffe2b11 CoreFoundation`CFDictionaryGetValue + 145
frame #2: 0x00007fff8dc55750 Foundation`NSKVOUnionSetAndNotify + 147
* frame #3: 0x0000000100000f85 TestApp`__19-[TestObject start]_block_invoke(.block_descriptor=<unavailable>) + 165 at main.m:34
frame #4: 0x000000010001832d libdispatch.dylib`_dispatch_call_block_and_release + 12
frame #5: 0x0000000100014925 libdispatch.dylib`_dispatch_client_callout + 8
frame #6: 0x0000000100016c3d libdispatch.dylib`_dispatch_root_queue_drain + 601
frame #7: 0x00000001000182e6 libdispatch.dylib`_dispatch_worker_thread2 + 52
frame #8: 0x00007fff9291eef8 libsystem_pthread.dylib`_pthread_wqthread + 314
frame #9: 0x00007fff92921fb9 libsystem_pthread.dylib`start_wqthread + 13
如果使用符号NSKVOUnionSetAndNotify
设置符号断点,则调试器将停止调用此方法的位置。
您看到的崩溃是因为在您调用[addFoos:]
方法时从一个线程发送自动键值通知,但是从另一个线程访问更改字典。调用此方法时使用全局调度队列会刺激这种情况,因为这会在许多不同的线程中执行该块。
在最简单的情况下,您可以使用此键的键值编码可变代理对象来修复崩溃:
NSMutableSet *someSet = [self mutableSetValueForKey:@"foos"];
[someSet unionSet:[NSSet setWithObject:@(rand() % 100)]];
这将阻止这种特殊的崩溃。这里发生了什么?调用mutableSetValueForKey:
时,结果是一个代理对象,它将消息转发给符合KVC标准的访问者方法,以便为密钥&#34; foos&#34 ;.作者的对象实际上并不完全符合此类型的KVC兼容属性所需的模式。如果为此密钥发送其他KVC访问器方法,它们可能会通过Foundation提供的非线程安全访问器,这可能会再次导致此崩溃。我们马上就会解决这个问题。
崩溃是由自动 KVO更改通知跨越线程触发的。自动KVO通知在运行时通过调配类和方法工作。您可以阅读更深入的解释here和here。 KVC访问器方法基本上在运行时用KVO提供的方法包装。事实上,这是原始应用程序崩溃的原因。这是从基金会反汇编的KVO插入代码:
int _NSKVOUnionSetAndNotify(int arg0, int arg1, int arg2) {
r4 = object_getIndexedIvars(object_getClass(arg0));
OSSpinLockLock(_NSKVONotifyingInfoPropertyKeysSpinLock);
r6 = CFDictionaryGetValue(*(r4 + 0xc), arg1);
OSSpinLockUnlock(_NSKVONotifyingInfoPropertyKeysSpinLock);
var_0 = arg2;
[arg0 willChangeValueForKey:r6 withSetMutation:0x1 usingObjects:STK-1];
r0 = *r4;
r0 = class_getInstanceMethod(r0, arg1);
method_invoke(arg0, r0);
var_0 = arg2;
r0 = [arg0 didChangeValueForKey:r6 withSetMutation:0x1 usingObjects:STK-1];
Pop();
Pop();
Pop();
return r0;
}
如您所见,这是使用willChangeValueForKey:withSetMutation:usingObjects:
和didChangeValueForKey: withSetMutation:usingObjects:
包装符合KVC的访问器方法。这些是发送KVO通知的方法。如果对象选择了自动键值观察器通知,KVO将在运行时插入此包装器。在这些来电之间,您可以看到class_getInstanceMethod
。这是对被包装的KVC兼容访问器的引用,然后调用它。对于原始代码,这是从NSSet的unionSet:
内部触发的,这是跨线程发生的,并在访问更改字典时导致崩溃。
自动通知由发生更改的线程发送,并且旨在在同一线程上接收。这就是Teh IntarWebs,关于KVO有很多不好或误导性的信息。并非所有对象都会发出自动KVO通知,并且在您的课程中,您可以控制执行和不执行的操作。来自Key Value Observing Programming Guide: Automatic Change Notification:
NSObject提供自动键值更改通知的基本实现。自动键值更改通知向观察者通知使用键值兼容访问器所做的更改,以及键值编码方法。返回的集合代理对象也支持自动通知,例如mutableArrayValueForKey:
这可能导致人们相信NSObject的所有后代默认发出自动通知。事实并非如此 - 框架类可能没有,或者实现特殊行为。核心数据就是一个例子。来自Core Data Programming Guide:
NSManagedObject禁用建模属性的自动键值观察(KVO)更改通知,并且原始访问器方法不会调用访问和更改通知方法。对于未建模的属性,在OS X v10.4上,Core Data也会禁用自动KVO;在OS X v10.5及更高版本中,Core Data采用了NSObject的行为。
作为开发人员,您可以通过使用正确的命名约定+automaticallyNotifiesObserversOf<Key>
实现方法,确保为特定属性启用或禁用自动键值观察器通知。当此方法返回NO时,不会为此属性发出自动键值通知。当禁用自动更改通知时,KVO也不必在运行时调用访问器方法,因为这主要是为了支持自动更改通知。例如:
+ (BOOL) automaticallyNotifiesObserversOfFoos {
return NO;
}
在评论中,作者表示他使用dispatch_barrier_sync
作为其访问者方法的原因是,如果他没有,KVO通知将在更改发生之前到达。通过为属性禁用自动通知,您仍然可以选择手动发送这些通知。这是通过使用方法willChangeValueForKey:
和didChangeValueForKey:
来完成的。这不仅可以控制何时发送这些通知(如果有的话),还可以控制什么线程。您记得,自动更改通知是在发生更改的线程上发送和接收的。
例如,如果您希望更改通知仅在主队列上发生 ,则可以使用递归分解来执行此操作:
- (void)addFoos:(NSSet *)objects {
dispatch_async(dispatch_get_main_queue(), ^{
[self willChangeValueForKey:@"foos"];
dispatch_barrier_sync(queue, ^{
[_internalFoos unionSet:objects];
dispatch_async(dispatch_get_main_queue(), ^{
[self didChangeValueForKey:@"foos"];
});
});
});
}
作者问题中的原始类强迫KVO观察在主队列上启动和停止,这似乎是尝试在主队列上发出通知。上面的示例演示了一个解决方案,该解决方案不仅解决了这一问题,还确保在数据更改之前和之后正确发送KVO通知。
在上面的例子中,我修改了作者的原始方法作为一个说明性的例子 - 这个类仍然没有正确的KVC兼容键&#34; foos&#34;。要符合Key-Value Observing,对象必须首先符合键值编码。要解决此问题,请先创建correct Key-value coding compliant accessors for an unordered mutable collection:
不可变:
countOfFoos
enumeratorOfFoos
memberOfFoos:
可变的:
addFoosObject:
removeFoosObject:
这些只是最小的,可以出于性能或数据完整性原因而实施其他方法。
原始应用程序使用并发队列dispatch_barrier_sync
。由于许多原因,这很危险。 Concurrency Programming Guide建议的方法是使用串行队列。这确保了一次只能触摸受保护资源的一件事,并且它来自一致的上下文。例如,上述两种方法看起来像这样:
- (NSUInteger)countOfFoos {
__block NSUInteger result = 0;
dispatch_sync([self serialQueue], ^{
result = [[self internalFoos] count];
});
return result;
}
- (void) addFoosObject:(id)object {
id addedObject = [object copy];
dispatch_async([self serialQueue], ^{
[[self internalFoos] addObject:addedObject];
});
}
请注意,在此示例和下一个示例中,为了简洁和清晰起见,我不包括手动KVO更改通知。如果您希望发送手动更改通知,则应将这些代码添加到这些方法中,就像您在上一个示例中看到的那样。
与将dispatch_barrier_sync
与并发队列一起使用不同,这不会导致死锁。
WWDC 2011 Session 210 Mastering Grand Central Dispatch显示正确使用调度屏障API,以使用并发队列实现集合的读取器/写入器锁定。这可以这样实现:
- (id) memberOfFoos:(id)object {
__block id result = nil;
dispatch_sync([self concurrentQueue], ^{
result = [[self internalFoos] member:object];
});
return result;
}
- (void) addFoosObject:(id)object {
id addedObject = [object copy];
dispatch_barrier_async([self concurrentQueue], ^{
[[self internalFoos] addObject:addedObject];
});
}
请注意,对于写入操作,异步访问调度屏障,而读取操作使用dispatch_sync
。原始应用程序使用dispatch_barrier_sync
进行读取和写入,作者声明这样做是为了控制何时发送自动更改通知。使用手动更改通知将解决该问题(同样,为简洁和清晰起见,此示例中未显示)。
原始版本中的KVO实施仍存在问题。它不使用context
指针来确定观察的所有权。这是推荐的做法,可以使用指向self
的指针作为值。该值应与用于添加和删除观察者的对象具有相同的地址:
[self addObserver:self forKeyPath:@"foos" options:NSKeyValueObservingOptionNew context:(void *)self];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == (__bridge void *)self){
// check the key path, etc.
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
来自NSKeyValueObserving.h标题:
您应该使用-removeObserver:forKeyPath:context:而不是-removeObserver:forKeyPath:尽可能使用它,因为它允许您更精确地指定您的意图。当同一个观察者多次注册相同的密钥路径,但每次使用不同的上下文指针时,-removeObserver:forKeyPath:在决定要删除的内容时必须猜测上下文指针,并且它可以猜错。
如果您有兴趣进一步了解应用和实施键值观察,我建议视频KVO Considered Awesome
•实施所需的key-value coding accessor pattern(无序可变集合)
•使这些访问者线程安全(使用带有dispatch_sync
/ dispatch_async
的串行队列,或带有dispatch_sync
/ dispatch_barrier_async
的并发队列
•决定是否需要自动KVO通知,相应地实施automaticallyNotifiesObserversOfFoos
•将适当的手动更改通知添加到访问者方法
•确保通过正确的KVC访问器方法(即mutableSetValueForKey:
)访问您的媒体资源的代码