为什么这个KVO代码会在100%的时间内崩溃?

时间:2014-09-14 12:40:34

标签: ios objective-c key-value-observing

下面的代码会在调用NSKVOUnionSetAndNotify的{​​{1}}内部崩溃,其中包含的内容似乎是虚假字典。

似乎是混合的CFDictionaryGetValue / addFoos代码与添加和删除KVO观察者的行为之间的竞争。

NSKVOUnionSetAndNotify

1 个答案:

答案 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通知在运行时通过调配类和方法工作。您可以阅读更深入的解释herehere。 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与并发队列一起使用不同,这不会导致死锁。

This is how you get deadlocks

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:)访问您的媒体资源的代码