Ios Firebase FSnapshotUtilities由于在枚举时变异NSMutableDictionary而崩溃

Ios Firebase FSnapshotUtilities由于在枚举时变异NSMutableDictionary而崩溃,ios,objective-c,firebase,Ios,Objective C,Firebase,编辑根据接受的答案,解决方案是使用mutableDeepCopy。对于发送到Firebase的设置值的任何值,以及观察到的更改返回的任何值,都需要使用此选项。这是Firebase SDK的已知问题,应该很快修复 @interface NSDictionary (DeepCopy) - (NSDictionary*)mutableDeepCopy { return (NSMutableDictionary *)CFBridgingRelease(CFPropertyListCreateDe

编辑根据接受的答案,解决方案是使用
mutableDeepCopy
。对于发送到Firebase的
设置值
的任何值,以及观察到的更改返回的任何值,都需要使用此选项。这是Firebase SDK的已知问题,应该很快修复

@interface NSDictionary (DeepCopy) 

- (NSDictionary*)mutableDeepCopy {
  return (NSMutableDictionary *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)self, kCFPropertyListMutableContainers));
}

@end

我正在开发一个用于实时协作的应用程序。Firebase库由于竞争条件而间歇性崩溃,其中它在枚举时变异了
NSMutableDictionary
。我在这里发布它是为了提高可视性,同时Firebase更喜欢使用堆栈溢出作为bug报告的主要方法

*** Collection <__NSDictionaryM: 0xd8198f0> was mutated while being enumerated.
2014-04-27 09:39:45.328 SharedNotesPro[29350:870b] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSDictionaryM: 0xd8198f0> was mutated while being enumerated.'
*** First throw call stack:
(
    0   CoreFoundation                      0x044711e4 __exceptionPreprocess + 180
    1   libobjc.A.dylib                     0x03f3e8e5 objc_exception_throw + 44
    2   CoreFoundation                      0x04500cf5 __NSFastEnumerationMutationHandler + 165
    3   SharedNotesPro                      0x003fe8f5 +[FSnapshotUtilities nodeFrom:withPriority:] + 1405
    4   SharedNotesPro                      0x003fe373 +[FSnapshotUtilities nodeFrom:] + 51
    5   SharedNotesPro                      0x003fe971 +[FSnapshotUtilities nodeFrom:withPriority:] + 1529
    6   SharedNotesPro                      0x003e2504 -[FRepo setInternal:newVal:withPriority:withCallback:andPutId:] + 298
    7   SharedNotesPro                      0x003e23af -[FRepo set:withVal:withPriority:withCallback:] + 165
    8   SharedNotesPro                      0x00402aaf __61-[Firebase setValueInternal:andPriority:withCompletionBlock:]_block_invoke + 174
    9   libdispatch.dylib                   0x047a07b8 _dispatch_call_block_and_release + 15
    10  libdispatch.dylib                   0x047b54d0 _dispatch_client_callout + 14
    11  libdispatch.dylib                   0x047a3047 _dispatch_queue_drain + 452
    12  libdispatch.dylib                   0x047a2e42 _dispatch_queue_invoke + 128
    13  libdispatch.dylib                   0x047a3de2 _dispatch_root_queue_drain + 78
    14  libdispatch.dylib                   0x047a4127 _dispatch_worker_thread2 + 39
    15  libsystem_pthread.dylib             0x04ae4dab _pthread_wqthread + 336
    16  libsystem_pthread.dylib             0x04ae8cce start_wqthread + 30
)
libc++abi.dylib: terminating with uncaught exception of type NSException

编辑:这应该不再是一个问题,因为最新的Firebase SDK将在setValue调用中同步克隆您的对象。在将数据传递给Firebase之前,不再需要手动克隆数据

尽管您正在调用“复制”,但这只会对最外层的NSDictionary进行“浅层”复制,因此如果您在外部NSDictionary中有任何NSDictionary,并且您正在修改这些NSDictionary,那么当Firebase枚举这些内部NSDictionary对象时,我们仍然可以从调用堆栈中遇到此错误,看起来我们是在列举一个内在的

Firebase真的应该自动为您制作此副本,这样您就不必担心了。我们打开了一个bug来解决这个问题。但现在,你需要做一个“深度拷贝”,而不是浅拷贝。有关一些可能的方法,请参见此处:(第二个或第三个答案看起来很有可能)。

编辑: 我相信我已经找到了异常的潜在原因:

我有一种预感,多个事务试图在同一个节点上本地运行,并且由于高堆栈跟踪而导致争用。最后,我将当前正在运行的事务保存在一个集合中,并在启动另一个事务之前在节点上测试正在运行的事务。代码如下:

@interface MyViewController ()

@property (nonatomic, strong) NSMutableSet *transactions;   // holds transactions to prevent contention
@property (nonatomic, strong) NSMutableDictionary *values;  // holds most recent values to avoid callback roundtrip

@end

@implementation MyViewController

-(NSArray*)firebasePathTokens:(Firebase*)firebase
{
    NSMutableArray  *tokens = [NSMutableArray array];

    while(firebase.name)
    {
        [tokens insertObject:firebase.name atIndex:0];

        firebase = firebase.parent;
    }

    return tokens;
}

// workaround for private firebase.path
-(NSString*)firebasePath:(Firebase*)firebase
{
    return firebase ? [@"/" stringByAppendingString:[[self firebasePathTokens:firebase] componentsJoinedByString:@"/"]] : nil;
}

- (void)runTransaction:(Firebase*)firebase
{
    NSString    *firebasePath = [self firebasePath:firebase];

    if([self.transactions containsObject:firebasePath])
    {
        NSLog(@"transaction already in progress: %@", firebasePath);

        return;
    }

    [self.transactions addObject:firebasePath];

    NSNumber    *myValue = @(42);

    [firebase runTransactionBlock:^FTransactionResult *(FMutableData *currentData) {
        currentData.value = myValue;

        return [FTransactionResult successWithValue:currentData];
    } andCompletionBlock:^(NSError *error, BOOL committed, FDataSnapshot *snapshot) {

        values[firebasePath] = snapshot.value;  // short example for brevity, the value should really be merged into a hierarchy of NSMutableDictionary at the appropriate node

        [self.transactions removeObject:firebasePath];
    } withLocalEvents:NO];
}

@end

我也遇到了这个问题,这是我的堆栈跟踪:

2014-05-01 12:18:31.641 MY_APP_NAME______[6076:60b] {
    UncaughtExceptionHandlerAddressesKey = (
    0   CoreFoundation                      0x030131e4 __exceptionPreprocess + 180
    1   libobjc.A.dylib                     0x02d928e5 objc_exception_throw + 44
    2   CoreFoundation                      0x030a2cf5 __NSFastEnumerationMutationHandler + 165
    3   MY_APP_NAME______                   0x000ecf53 -[FTree forEachChild:] + 290
    4   MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    5   MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    6   MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    7   MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    8   MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    9   MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    10  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    11  MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    12  MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    13  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    14  MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    15  MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    16  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    17  MY_APP_NAME______                   0x001127ea -[FRepo(Transaction) rerunTransactionQueue:atPath:] + 2888
    18  MY_APP_NAME______                   0x00111a7f -[FRepo(Transaction) rerunTransactionsAndUpdateVisibleDataForPath:] + 422
    19  MY_APP_NAME______                   0x001114c7 __50-[FRepo(Transaction) sendTransactionQueue:atPath:]_block_invoke + 3092
    20  MY_APP_NAME______                   0x000e61d6 -[FPersistentConnection ackPuts] + 286
    21  MY_APP_NAME______                   0x000e492a __38-[FPersistentConnection sendListen:::]_block_invoke + 778
    22  MY_APP_NAME______                   0x000e268a -[FPersistentConnection onDataMessage:withMessage:] + 465
    23  MY_APP_NAME______                   0x000d733a -[FConnection onDataMessage:] + 106
    24  MY_APP_NAME______                   0x000d7293 -[FConnection onMessage:withMessage:] + 282
    25  MY_APP_NAME______                   0x000d4ba4 -[FWebSocketConnection appendFrame:] + 402
    26  MY_APP_NAME______                   0x000d4c73 -[FWebSocketConnection handleIncomingFrame:] + 161
    27  MY_APP_NAME______                   0x000d4cab -[FWebSocketConnection webSocket:didReceiveMessage:] + 40
    28  MY_APP_NAME______                   0x000cfbe1 __31-[FSRWebSocket _handleMessage:]_block_invoke + 151
    29  libdispatch.dylib                   0x0366f7b8 _dispatch_call_block_and_release + 15
    30  libdispatch.dylib                   0x036844d0 _dispatch_client_callout + 14
    31  libdispatch.dylib                   0x03672047 _dispatch_queue_drain + 452
    32  libdispatch.dylib                   0x03671e42 _dispatch_queue_invoke + 128
    33  libdispatch.dylib                   0x03672de2 _dispatch_root_queue_drain + 78
    34  libdispatch.dylib                   0x03673127 _dispatch_worker_thread2 + 39
    35  libsystem_pthread.dylib             0x039b3dab _pthread_wqthread + 336
    36  libsystem_pthread.dylib             0x039b7cce start_wqthread + 30
);
}
2014-05-01 12:18:35.897 MY_APP_NAME______[6076:3e07] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSDictionaryM: 0x7c93e260> was mutated while being enumerated.'
*** First throw call stack:
(
    0   CoreFoundation                      0x030131e4 __exceptionPreprocess + 180
    1   libobjc.A.dylib                     0x02d928e5 objc_exception_throw + 44
    2   CoreFoundation                      0x030a2cf5 __NSFastEnumerationMutationHandler + 165
    3   MY_APP_NAME______                   0x000ecf53 -[FTree forEachChild:] + 290
    4   MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    5   MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    6   MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    7   MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    8   MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    9   MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    10  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    11  MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    12  MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    13  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    14  MY_APP_NAME______                   0x00111898 __58-[FRepo(Transaction) pruneCompletedTransactionsBelowNode:]_block_invoke + 43
    15  MY_APP_NAME______                   0x000ed01d -[FTree forEachChild:] + 492
    16  MY_APP_NAME______                   0x0011184a -[FRepo(Transaction) pruneCompletedTransactionsBelowNode:] + 373
    17  MY_APP_NAME______                   0x001127ea -[FRepo(Transaction) rerunTransactionQueue:atPath:] + 2888
    18  MY_APP_NAME______                   0x00111a7f -[FRepo(Transaction) rerunTransactionsAndUpdateVisibleDataForPath:] + 422
    19  MY_APP_NAME______                   0x001114c7 __50-[FRepo(Transaction) sendTransactionQueue:atPath:]_block_invoke + 3092
    20  MY_APP_NAME______                   0x000e61d6 -[FPersistentConnection ackPuts] + 286
    21  MY_APP_NAME______                   0x000e492a __38-[FPersistentConnection sendListen:::]_block_invoke + 778
    22  MY_APP_NAME______                   0x000e268a -[FPersistentConnection onDataMessage:withMessage:] + 465
    23  MY_APP_NAME______                   0x000d733a -[FConnection onDataMessage:] + 106
    24  MY_APP_NAME______                   0x000d7293 -[FConnection onMessage:withMessage:] + 282
    25  MY_APP_NAME______                   0x000d4ba4 -[FWebSocketConnection appendFrame:] + 402
    26  MY_APP_NAME______                   0x000d4c73 -[FWebSocketConnection handleIncomingFrame:] + 161
    27  MY_APP_NAME______                   0x000d4cab -[FWebSocketConnection webSocket:didReceiveMessage:] + 40
    28  MY_APP_NAME______                   0x000cfbe1 __31-[FSRWebSocket _handleMessage:]_block_invoke + 151
    29  libdispatch.dylib                   0x0366f7b8 _dispatch_call_block_and_release + 15
    30  libdispatch.dylib                   0x036844d0 _dispatch_client_callout + 14
    31  libdispatch.dylib                   0x03672047 _dispatch_queue_drain + 452
    32  libdispatch.dylib                   0x03671e42 _dispatch_queue_invoke + 128
    33  libdispatch.dylib                   0x03672de2 _dispatch_root_queue_drain + 78
    34  libdispatch.dylib                   0x03673127 _dispatch_worker_thread2 + 39
    35  libsystem_pthread.dylib             0x039b3dab _pthread_wqthread + 336
    36  libsystem_pthread.dylib             0x039b7cce start_wqthread + 30
)
libc++abi.dylib: terminating with uncaught exception of type NSException
2014-05-01 12:18:49.810 MY_APP_NAME______[6076:60b] {
    UncaughtExceptionHandlerSignalKey = 6;
}
不幸的是,我认为Firebase存在一个问题,有时会导致以下顺序:

value on server: 41
setValue: 42
    error: permission error
    observeSingleEventOfType: 42    // returns the attempted value 42 instead of the previous value 41
value on server: 41
app proceeds to inappropriate state with wrong value 42
我认为现在发生的事情是,因为我在调用setValue之前从未调用过observeSingleEventOfType,所以当setValue未能满足Firebase规则时,Firebase没有以前的值可以依赖。因此,它返回尝试的值,而不是像null这样的“未定义”占位符。我不确定这是一个bug还是一个特性,但这是需要注意的。因此,我将该代码替换为以下代码:

[myFirebase runTransactionBlock:^FTransactionResult *(FMutableData *currentData) {
    currentData.value = myValue;

    return [FTransactionResult successWithValue:currentData];
} andCompletionBlock:^(NSError *error, BOOL committed, FDataSnapshot *snapshot) {
    myMostRecentValue = snapshot.value;
} withLocalEvents:NO];
这导致NSMutableDictionary在枚举异常时发生变异。奇怪的是,我只是为值传递了一个NSNumber,而不是试图在runTransactionBlock中设置自己的NSMutableDictionary。然而,myMostRecentValue在NSMutableDictionary中,但我只在andCompletionBlock中设置了它,所以它不重要

我能想到的唯一一件事是,有时可能有两个或多个事务在同一个节点上运行,或者一个事务在父节点上运行,而另一个事务在子节点上运行。这可能是因为如果没有卸载旧的视图控制器,我可能会在视图控制器之间切换时安装侦听器。这对我来说很难测试,所以这只是一个理论

不确定是否有帮助,但这里有一个mutableDeepCopy category函数,用于将值从Firebase复制到本地NSMutableDictionary,用于缓存最新的已知值(例如在observeSingleEventOfType回调中):

有时我需要避免viewDidLoad中的往返,因此我将最后一个已知值放在GUI元素中,直到得到新值的回调。我无法想象这会对Firebase造成影响,但可能是一些低级的东西在期待NSDictionary和Choke,因为它引用了我提供给它的NSMutableDictionary的一部分


在找到解决方案之前我有点困了,所以希望这能有所帮助,谢谢

糟糕的是,我拿走了我的东西。我误读了你的三元运算符。@Logan混乱可能是我写了一行很长的逻辑的错——对不起;)嗯,这确实很奇怪。从堆栈上看,它确实像是Firebase问题,但我不确定在枚举时为什么会发生变异。这不应该发生。我明天会看一看,然后再给你回复。ping@MichaelLehenbauer。。。运气好吗?@ZaneClaes很抱歉回复太慢了!:-(在再次查看并仔细思考之后,我怀疑我知道发生了什么。请参阅我刚刚提交的答案。这听起来是一个可能的答案,但我需要花费周末的压力测试来确认。这并没有解决问题。这是一个发生的屏幕截图:这是我的Firebase实现的更新要点:以下是显示所有线程正在执行的操作的屏幕截图。请注意,我的代码没有运行。至少有2个Firebase线程处于活动状态,但没有一个线程处于枚举/变异过程中,因此Firebase中的冲突似乎是100%(与我对对象的操作无关):我还尝试使用
[Firebase setDispatchQueue:dispatch_get_main_queue()]
。这并没有解决问题;
Firebase Worker
线程似乎仍然在后台线程上创建……FWIW,mutableDeepCopy可以大大简化:
返回(NSMutableDictionary*)CbrigingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault,(CFDictionaryRef)自身,kCFPropertyListMutableContainers)是的,谢谢你,在你的建议之后,我偶然发现了这一点,并实现了copyDeep、mutableCopyDeep和immutableCopyDeep,它们分别使用archivedDataWithRootObject、KCFPropertyListTableContainers和Leaves和kCFPropertyListImmutable。这样我就可以控制节点的可变性
value on server: 41
setValue: 42
    error: permission error
    observeSingleEventOfType: 42    // returns the attempted value 42 instead of the previous value 41
value on server: 41
app proceeds to inappropriate state with wrong value 42
[myFirebase runTransactionBlock:^FTransactionResult *(FMutableData *currentData) {
    currentData.value = myValue;

    return [FTransactionResult successWithValue:currentData];
} andCompletionBlock:^(NSError *error, BOOL committed, FDataSnapshot *snapshot) {
    myMostRecentValue = snapshot.value;
} withLocalEvents:NO];
// category to simplify getting a deep mutableCopy
@implementation NSDictionary(mutableDeepCopy)

- (NSMutableDictionary*)mutableDeepCopy
{
    NSMutableDictionary *returnDict = [[NSMutableDictionary alloc] initWithCapacity:self.count];

    for(id key in [self allKeys])
    {
        id oneValue = [self objectForKey:key];

        if([oneValue respondsToSelector:@selector(mutableDeepCopy)])
            oneValue = [oneValue mutableDeepCopy];
        else if([oneValue respondsToSelector:@selector(mutableCopy)] && ![oneValue isKindOfClass:[NSNumber class]]) // workaround for -[__NSCFNumber mutableCopyWithZone:]: unrecognized selector sent to instance
            oneValue = [oneValue mutableCopy];
        else
            oneValue = [oneValue copy];

        [returnDict setValue:oneValue forKey:key];
    }

    return returnDict;
}