ios - 为什么这个 KVO 代码会 100% 崩溃?

标签 ios objective-c key-value-observing

下面的代码将在 NSKVOUnionSetAndNotify 内崩溃调用 CFDictionaryGetValue似乎是一本伪造的字典。

这似乎是一场困惑的比赛addFoos/NSKVOUnionSetAndNotify代码以及添加和删除 KVO 观察者的行为。

#import <Foundation/Foundation.h>
@interface TestObject : NSObject
@property (readonly) NSSet *foos;
@end

@implementation TestObject {
    NSMutableSet *_internalFoos;
    dispatch_queue_t queue;
    BOOL observed;
}

- (id)init {
    self = [super init];
    _internalFoos = [NSMutableSet set];
    queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    return self;
}

- (void)start {
    // Start a bunch of work hitting the unordered collection mutator
    for (int i = 0; i < 10; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            while (YES) {
                @autoreleasepool {
                    [self addFoos:[NSSet setWithObject:@(rand() % 100)]];
                }
            }
        });
    }

    // Start work that will constantly observe and unobserve the unordered collection
    [self observe];
}

- (void)observe {
    dispatch_async(dispatch_get_main_queue(), ^{
        observed = YES;
        [self addObserver:self forKeyPath:@"foos" options:0 context:NULL];
    });
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    dispatch_async(dispatch_get_main_queue(), ^{
        if (observed) {
            observed = NO;
            [self removeObserver:self forKeyPath:@"foos"];
            [self observe];
        }
    });
}

// Public unordered collection property
- (NSSet *)foos {
    __block NSSet *result;
    dispatch_sync(queue, ^{
        result = [_internalFoos copy];
    });
    return result;
}

// KVO compliant mutators for unordered collection
- (void)addFoos:(NSSet *)objects {
    dispatch_barrier_sync(queue, ^{
        [_internalFoos unionSet:objects];
    });
}

- (void)removeFoos:(NSSet *)objects {
    dispatch_barrier_sync(queue, ^{
        [_internalFoos minusSet:objects];
    });
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TestObject *t = [[TestObject alloc] init];
        [t start];
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10000, false);
    }
    return 0;
}

最佳答案

你得到的实际崩溃是 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 兼容访问器方法,以获取 key “foos”。作者的对象实际上并不完全符合这种类型的 KVC 兼容属性所需的模式。如果其他 KVC 访问器方法为该 key 发送消息,它们可能会通过 Foundation 提供的非线程安全访问器,这可能会导致再次崩溃。稍后我们将讨论如何解决这个问题。

崩溃是由跨线程的自动 KVO 更改通知触发的。自动 KVO 通知通过在运行时混合类和方法来工作。您可以阅读更深入的解释herehere . KVC 访问器方法本质上是在运行时用 KVO 提供的方法包装的。这实际上是原始应用程序崩溃的地方。这是从 Foundation 反汇编的 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: 包装了一个 KVC 兼容的访问器方法。和 didChangeValueForKey: withSetMutation:usingObjects: .这些是发送 KVO 通知的方法。如果对象选择了自动键值观察器通知,KVO 将在运行时插入此包装器。在这些调用之间,您可以看到 class_getInstanceMethod .这是获取对被包装的 KVC 兼容访问器的引用,然后调用它。在原始代码的情况下,这是从 NSSet 的 unionSet: 内部触发的。 ,这是跨线程发生的,并在访问更改字典时导致崩溃。

自动通知由发生更改的线程发送,旨在在同一线程上接收。这就是 Teh IntarWebs,有很多关于 KVO 的不良或误导性信息。并非所有对象和属性都会发出自动 KVO 通知,并且在您的类中,您可以控制哪些可以做哪些不可以做。来自 Key Value Observing Programming Guide: Automatic Change Notification :

NSObject provides a basic implementation of automatic key-value change notification. Automatic key-value change notification informs observers of changes made using key-value compliant accessors, as well as the key-value coding methods. Automatic notification is also supported by the collection proxy objects returned by, for example, mutableArrayValueForKey:



这可能会让人相信 NSObject 的所有后代默认都会发出自动通知。情况并非如此 - 框架类可能没有,或者实现特殊行为。核心数据就是一个例子。来自 Core Data Programming Guide :

NSManagedObject disables automatic key-value observing (KVO) change notifications for modeled properties, and the primitive accessor methods do not invoke the access and change notification methods. For unmodeled properties, on OS X v10.4 Core Data also disables automatic KVO; on OS X v10.5 and later, Core Data adopts to NSObject’s behavior.



作为开发人员,您可以通过实现具有正确命名约定 +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 通知。

在上面的例子中,我修改了作者的原始方法作为一个说明性的例子——这个类对于键“foos”仍然不正确地符合 KVC。要符合键值观察,对象必须首先符合键值编码。为了解决这个问题,首先创建 correct Key-value coding compliant accessors for an unordered mutable collection :

不可变:countOfFoosenumeratorOfFoosmemberOfFoos:
可变:addFoosObject:removeFoosObject:
这些只是最低要求,出于性能或数据完整性的原因,还可以实现其他方法。

原始应用程序使用并发队列和 dispatch_barrier_sync .这很危险,原因有很多。 Concurrency Programming Guide推荐的方法是改为使用串行队列。这确保了一次只有一件事可以接触 protected 资源,并且它来自一致的上下文。例如,上述两种方法如下所示:
- (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];
    });
}

Note that in this example and the next, I am not including manual KVO change notifications for brevity and clarity. If you want manual change notifications to be sent, that code should be added to these methods like what you saw in the previous example.



不像使用 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 header :

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.



如果您有兴趣进一步了解应用和实现 Key Value Observing,我建议观看视频 KVO Considered Awesome

总之:

• 实现所需的key-value coding accessor pattern (无序可变集合)

• 使这些访问器线程安全(使用带有 dispatch_sync/dispatch_async 的串行队列,或带有 dispatch_sync/dispatch_barrier_async 的并发队列)

• 决定是否需要自动 KVO 通知,实现 automaticallyNotifiesObserversOfFoos因此

• 适本地向访问器方法添加手动更改通知

• 确保访问您的 Assets 的代码通过正确的 KVC 访问器方法(即 mutableSetValueForKey:)进行访问

关于ios - 为什么这个 KVO 代码会 100% 崩溃?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25833322/

相关文章:

ios - iOS Swift 中的观察者、 Action 监听器、KVO

ios - 在 KVO 中使用嵌套键路径

ios - 如何使用关联数组数据源填充表头? swift XCode 6.4

ios - 触发从表格单元格到另一页的 Segue

iOS 7 TableView 类似于 iPad 上的设置应用程序

iOS:检查 coredata 对象是否仍然存在?

ios - 如何摆脱 UIToolBar 中我的按钮顶部的线

ios - NSURLSessionDownloadTasks 在设备自动锁定或后台事件 30 分钟后停止

ios - iPhone - 内存泄漏 - NSData dataWithContentsOfUrl 和 UIWebView

javascript - Angular 2+ 检测服务内部的对象属性更改