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

 会唱歌的高跟鞋 发布于 2022-12-21 14:05

下面的代码将在NSKVOUnionSetAndNotify调用内部崩溃,CFDictionaryGetValue看起来似乎是一个虚假的字典.

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

#import 
@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;
}

quellish.. 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=) + 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-存取方法为重点"的Foo".作者的对象实际上并不完全符合此类型的KVC兼容属性所需的模式.如果为此密钥发送其他KVC访问器方法,它们可能会通过Foundation提供的非线程安全访问器,这可能会再次导致此崩溃.我们将在一瞬间了解如何解决这个问题.

崩溃是由跨越线程的自动 KVO更改通知触发的.自动KVO通知在运行时通过调配类和方法工作.您可以在此处和此处阅读更深入的解释.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:和包装符合KVC的访问器方法didChangeValueForKey: withSetMutation:usingObjects:.这些是发送KVO通知的方法.如果对象选择了自动键值观察器通知,KVO将在运行时插入此包装器.在这些电话之间你可以看到class_getInstanceMethod.这是对被包装的KVC兼容访问器的引用,然后调用它.在原始代码的情况下,这是从NSSet内部触发的unionSet:,这是跨线程发生的,并在访问更改字典时导致崩溃.

自动通知由发生更改的线程发送,并且旨在在同一线程上接收.这就是Teh IntarWebs,关于KVO有很多不好或误导性的信息.并非所有对象都发出自动KVO通知,并且在您的类中,您可以控制哪些对象和不可用.从键值观察编程指南:自动更改通知:

NSObject提供自动键值更改通知的基本实现.自动键值更改通知向观察者通知使用键值兼容访问器所做的更改,以及键值编码方法.由例如mutableArrayValueForKey返回的集合代理对象也支持自动通知:

这可能导致人们相信NSObject的所有后代默认发出自动通知.事实并非如此 - 框架类可能没有,或者实现特殊行为.核心数据就是一个例子.来自核心数据编程指南:

NSManagedObject禁用建模属性的自动键值观察(KVO)更改通知,并且原始访问器方法不会调用访问和更改通知方法.对于未建模的属性,在OS X v10.4上,Core Data也会禁用自动KVO; 在OS X v10.5及更高版本中,Core Data采用了NSObject的行为.

作为开发人员,您可以通过实现具有正确命名约定的方法来确保为特定属性启用或禁用自动键值观察器通知+automaticallyNotifiesObserversOf.当此方法返回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兼容键"foos".要符合Key-Value Observing,对象必须首先符合键值编码.要解决这个问题,首先要为无序的可变集合创建正确的符合键值编码的访问器:

一成不变的: countOfFoos enumeratorOfFoos memberOfFoos:

易变的: addFoosObject: removeFoosObject:

这些只是最小的,可以出于性能或数据完整性的原因实现其他方法.

原始应用程序使用并发队列和dispatch_barrier_sync.由于许多原因,这很危险." 并发编程指南"建议的方法是使用串行队列.这确保了一次只能触摸受保护资源的一件事,并且它来自一致的上下文.例如,上面的两个方法看起来像这样:

- (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考虑到真棒

综上所述:

•实现所需的键值编码访问器模式(无序可变集合)

•请那些访问线程安全的(使用串行队列dispatch_sync/ dispatch_async或并发队列dispatch_sync/ dispatch_barrier_async)

•据此决定是否要实现自动志愿通知与否,实施automaticallyNotifiesObserversOfFoos相应的

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

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

撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有