简体   繁体   English

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

[英]Why does this KVO code crash 100% of the time?

The code below will crash inside of NSKVOUnionSetAndNotify calling CFDictionaryGetValue with what appears to be a bogus dictionary. 下面的代码将在NSKVOUnionSetAndNotify内部崩溃,调用CFDictionaryGetValue ,看起来似乎是一个伪造的字典。

It seems to be a race between the swizzled addFoos / NSKVOUnionSetAndNotify code and the act of adding and removing KVO observers. 它似乎是混合的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;
}

The actual crash you get is an EXC_BAD_ACCESS when the key value observing dictionary is accesed. 当加入键值观察字典时,您获得的实际崩溃是EXC_BAD_ACCESS The stack trace is as follows: 堆栈跟踪如下:

* 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

If you set a symbolic breakpoint with the symbol NSKVOUnionSetAndNotify the debugger will stop where this method is being invoked. 如果使用符号NSKVOUnionSetAndNotify设置符号断点,则调试器将停止调用此方法的位置。 The crash you are seeing is because automatic key-value notifications are being sent from one thread when you invoke your [addFoos:] method, but then the change dictionary is being accessed from another thread. 您看到的崩溃是因为在您调用[addFoos:]方法时从一个线程发送自动键值通知,但是从另一个线程访问更改字典。 This is stimulated by your use of the global dispatch queue when calling this method, as that will execute the block in many different threads. 调用此方法时使用全局调度队列会刺激这种情况,因为这会在许多不同的线程中执行该块。

There are mulitple ways to fix this crash, and I will try to walk you through this to give you a more thourough understanding of what is going on. 有多种方法可以解决这个崩溃问题,我将尽力指导您,让您对正在发生的事情有更深入的了解。

In the simplest case, you can fix the crash by using the key-value coding mutable proxy object for this key: 在最简单的情况下,您可以通过使用此键的键值编码可变代理对象来修复崩溃:

NSMutableSet *someSet = [self mutableSetValueForKey:@"foos"];
[someSet unionSet:[NSSet setWithObject:@(rand() % 100)]];

That will stop this particular crash. 这将阻止这种特殊的崩溃。 What's happening here? 这里发生了什么事? When mutableSetValueForKey: is called, the result is a proxy object that forwards messages to your KVC-compliant accessor methods for the key "foos". 当调用mutableSetValueForKey: ,结果是一个代理对象,它将消息转发到符合KVC的访问器方法以获取密钥“foos”。 The author's object does not actually fully conform to the required pattern for a KVC compliant property of this type. 作者的对象实际上并不完全符合此类型的KVC兼容属性所需的模式。 If other KVC accessor methods are messaged for this key, they may go through non-thread safe accessors provided by Foundation, which can result in this crash all over again. 如果为此密钥发送其他KVC访问器方法,它们可能会通过Foundation提供的非线程安全访问器,这可能会再次导致此崩溃。 We'll get to how to fix that in a moment. 我们将在一瞬间了解如何解决这个问题。

The crash is being triggered by automatic KVO change notifications crossing threads. 崩溃是由跨越线程的自动 KVO更改通知触发的。 Automatic KVO notifications work by swizzling classes and methods at runtime. 自动KVO通知在运行时通过调配类和方法工作。 You can read a more in-depth explanation here and here . 您可以在此处此处阅读更深入的解释。 KVC accessor methods are essentially wrapped at runtime with KVO-supplied methods. KVC访问器方法基本上在运行时用KVO提供的方法包装。 This is in fact where the crash in the original application is happening. 事实上,这是原始应用程序崩溃的原因。 This is the KVO inserted code disassembled from 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;
}

As you can see, this is wrapping a KVC compliant accessor method with willChangeValueForKey:withSetMutation:usingObjects: and didChangeValueForKey: withSetMutation:usingObjects: . 正如您所看到的,这是使用willChangeValueForKey:withSetMutation:usingObjects:包装符合KVC的访问器方法willChangeValueForKey:withSetMutation:usingObjects:didChangeValueForKey: withSetMutation:usingObjects: . These are the methods that send out KVO notifications. 这些是发送KVO通知的方法。 KVO will insert this wrapper at runtime if the object has opted into automatic key value observer notification. 如果对象选择了自动键值观察器通知,KVO将在运行时插入此包装器。 In between these calls you can see class_getInstanceMethod . 在这些调用之间,您可以看到class_getInstanceMethod This is getting a reference to the KVC compliant accessor being wrapped, and then calling it. 这是对被包装的KVC兼容访问器的引用,然后调用它。 In the case of the original code, this is being triggered from inside NSSet's unionSet: , which was happening across threads and causing the crash when it accessed the change dictionary. 在原始代码的情况下,这是从NSSet的unionSet:内部触发的,它发生在线程之间,并在访问更改字典时导致崩溃。

Automatic notifications are sent by the thread where the change occured, and are intended to be received on the same thread. 自动通知由发生更改的线程发送,并且旨在在同一线程上接收。 This being Teh IntarWebs, there is a lot of bad or misleading information out there about KVO. 这就是Teh IntarWebs,关于KVO有很多不好或误导性的信息。 Not all objects and not all properties emit automatic KVO notifications, and in your classes you can control which do and don't. 并非所有对象都发出自动KVO通知,并且在您的类中,您可以控制哪些对象和不可用。 From the Key Value Observing Programming Guide: Automatic Change Notification : 键值观察编程指南:自动更改通知

NSObject provides a basic implementation of automatic key-value change notification. NSObject提供自动键值更改通知的基本实现。 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: 由例如mutableArrayValueForKey返回的集合代理对象也支持自动通知:

This may lead one to believe that all descendants of NSObject emit automatic notifications by default. 这可能导致人们相信NSObject的所有后代默认发出自动通知。 This is not the case - may framework classes do not, or implement special behavior. 事实并非如此 - 框架类可能没有,或者实现特殊行为。 Core Data is an example. 核心数据就是一个例子。 From the 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. NSManagedObject禁用建模属性的自动键值观察(KVO)更改通知,并且原始访问器方法不会调用访问和更改通知方法。 For unmodeled properties, on OS X v10.4 Core Data also disables automatic KVO; 对于未建模的属性,在OS X v10.4上,Core Data也会禁用自动KVO; on OS X v10.5 and later, Core Data adopts to NSObject's behavior. 在OS X v10.5及更高版本中,Core Data采用了NSObject的行为。

As a developer, you can ensure that automatic key value observer notifications are on or off for a particular property by implementing a method with the correct naming convention, +automaticallyNotifiesObserversOf<Key> . 作为开发人员,您可以通过实现具有正确命名约定的方法来确保为特定属性启用或禁用自动键值观察器通知, +automaticallyNotifiesObserversOf<Key> When this method returns NO, automatic key value notifications are not emitted for this property. 当此方法返回NO时,不会为此属性发出自动键值通知。 When automatic change notifications are disabled KVO also does not have to swizzle the accessor method at runtime, as this is done primarily to support automatic change notifications. 当禁用自动更改通知时,KVO也不必在运行时调用访问器方法,因为这主要是为了支持自动更改通知。 For example: 例如:

+ (BOOL) automaticallyNotifiesObserversOfFoos {
    return NO;
}

In a comment the author stated that the reason he was using dispatch_barrier_sync for his accessor methods is that if he did not, KVO notifications would arrive before changes occured. 在评论中,作者表示他使用dispatch_barrier_sync作为其访问器方法的原因是,如果他没有,KVO通知将在发生更改之前到达。 With automatic notifications disabled for a property, you still have the option of sending these notifications manually . 通过为属性禁用自动通知,您仍然可以选择手动发送这些通知。 This is done by using the methods willChangeValueForKey: and didChangeValueForKey: . 这是通过使用方法willChangeValueForKey:didChangeValueForKey: Not only does this give you control of when these notifications are sent (if at all), but on what thread . 这不仅可以控制何时发送这些通知(如果有的话),还可以控制在什么线程上 Automatic change notifications, as you recall, are sent from and received on the thread where the change occured. 您记得,自动更改通知是在发生更改的线程上发送和接收的。 For example, if you wanted change notifications to happen only on the main queue, you could do so using recursive decomposition: 例如,如果您希望更改通知在主队列上发生,则可以使用递归分解:

- (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"];
            });
        });
    });
}

The original class in the author's question was forcing KVO observation to start and stop on the main queue, which seems have been an attempt to emit notifications on the main queue. 作者问题中的原始类强迫KVO观察在主队列上启动和停止,这似乎是尝试在主队列上发出通知。 The above example demonstrates a solution that not only addresses that concern, but ensures that the KVO notifications are correctly sent before and after the data changes. 上面的示例演示了一个解决方案,该解决方案不仅解决了这一问题,还确保在数据更改之前和之后正确发送KVO通知。

In the example above I modified the author's original method as an illustrative example - this class is still not correctly KVC compliant for the key "foos". 在上面的例子中,我修改了作者的原始方法作为一个说明性的例子 - 这个类仍然没有正确的KVC兼容键“foos”。 To be Key-Value Observing compliant, an object must first be Key-Value Coding compliant. 要符合Key-Value Observing,对象必须首先符合键值编码。 To address this, first create the correct Key-value coding compliant accessors for an unordered mutable collection : 要解决这个问题,首先要为无序的可变集合创建正确的符合键值编码的访问器

Immutable: countOfFoos enumeratorOfFoos memberOfFoos: 不可变: countOfFoos enumeratorOfFoos memberOfFoos:

Mutable: addFoosObject: removeFoosObject: Mutable: addFoosObject: removeFoosObject:

These are just the minimum, there are additional methods that can be implemented for performance or data integrity reasons. 这些只是最小的,可以出于性能或数据完整性的原因实现其他方法。

The original application was using a concurrent queue and dispatch_barrier_sync . 原始应用程序使用并发队列和dispatch_barrier_sync This was dangerous, for many reasons. 由于许多原因,这很危险。 The approach recommended by the Concurrency Programming Guide is to instead use a serial queue. 并发编程指南”建议的方法是使用串行队列。 This ensures that only one thing can be touching the protected resource at a time, and it is from a consistent context. 这确保了一次只能触摸受保护资源的一件事,并且它来自一致的上下文。 For example, two of the above methods would look like this: 例如,上面的两个方法看起来像这样:

- (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. 请注意,在此示例和下一个示例中,为了简洁和清晰起见,我不包括手动KVO更改通知。 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. 如果要发送手动更改通知,则应将这些代码添加到这些方法中,就像您在上一个示例中看到的那样。

Unlike using dispatch_barrier_sync with a concurrent queue, this will not allow a deadlock. 与将dispatch_barrier_sync与并发队列一起使用不同,这将不允许死锁。

这就是你遇到僵局的方法

The WWDC 2011 Session 210 Mastering Grand Central Dispatch showed the correct use of the dispatch barrier API for implementing a reader/writer lock for a collection using a concurrent queue. WWDC 2011 Session 210 掌握Grand Central Dispatch显示正确使用调度屏障API,以使用并发队列实现集合的读取器/写入器锁定。 This would be implemented like this: 这将实现如下:

- (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];
    });
}

Note that the dispatch barrier is accessed asynchronously for the write operation, while the read operation uses dispatch_sync . 请注意,对于写入操作,异步访问调度屏障,而读取操作使用dispatch_sync The original application used dispatch_barrier_sync for both reads and writes, which the author stated was done to control when automatic change notifications were sent. 原始应用程序使用dispatch_barrier_sync进行读取和写入,作者声明这样做是为了控制何时发送自动更改通知。 Using manual change notifications would address that concern (again, not shown in this example for brevity and clarity). 使用手动更改通知将解决该问题(再次,为了简洁和清楚起见,在该示例中未示出)。

There are still issues with the KVO implementation in the original. 原始版本中的KVO实施仍然存在问题。 It does not use the context pointer to determine ownership of an observation. 它不使用context指针来确定观察的所有权。 This is a recommended practice, and can use a pointer to self as a value. 这是一种推荐的做法,可以使用指向self的指针作为值。 The value should have the same address as the objected used to add and remove the observer: 该值应与用于添加和删除观察者的对象具有相同的地址:

[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];
    }
}

From the NSKeyValueObserving.h header: 从NSKeyValueObserving.h标头:

You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. 您应该尽可能使用-removeObserver:forKeyPath:context:而不是-removeObserver:forKeyPath:因为它允许您更精确地指定您的意图。 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. 当同一个观察者多次注册相同的密钥路径,但每次使用不同的上下文指针时,-removeObserver:forKeyPath:在决定要删除的内容时必须猜测上下文指针,并且它可能猜错了。

If you are interested in a further understanding of applying and implementing Key Value Observing, I suggest the video KVO Considered Awesome 如果您有兴趣进一步了解应用和实施键值观察,我建议视频KVO考虑到真棒

In summary: 综上所述:

• Implement the required key-value coding accessor pattern (unordered mutable collection) •实现所需的键值编码访问器模式 (无序可变集合)

• Make those accessors thread safe (using a serial queue with dispatch_sync / dispatch_async , or a concurrent queue with dispatch_sync / dispatch_barrier_async ) •请那些访问线程安全的(使用串行队列dispatch_sync / dispatch_async ,或并发队列dispatch_sync / dispatch_barrier_async

• Decide wether you want automatic KVO notifications or not, implement automaticallyNotifiesObserversOfFoos accordingly •决定是否需要自动KVO通知,相应地automaticallyNotifiesObserversOfFoos实现通知ObserversOfFoos

• Add manual change notifications appropriately to accessor methods •适当地向访问者方法添加手动更改通知

• Make sure that code which accesses your property does so through the correct KVC accessor methods (ie mutableSetValueForKey: ) •确保访问您的属性的代码通过正确的KVC访问器方法(即mutableSetValueForKey:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM