Firebase FSnapshotUtilities由于在枚举时改变了NSMutableDictionary而崩溃

时间:2014-04-27 16:49:38

标签: ios objective-c firebase

编辑根据接受的答案,解决方案是使用mutableDeepCopy。您需要将此值用于发送到Firebase setValue的任何值,以及观察更改后返回的任何值。这是Firebase SDK的一个已知问题,应尽快解决。

@interface NSDictionary (DeepCopy) 

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

@end

我正在使用Firebase开发一个用于实时协作的应用程序。 Firebase库由于竞争条件而间歇性崩溃,在枚举NSMutableDictionary的同时枚举它。我在这里发布它是为了可见性,以及Firebase更喜欢使用Stack Overflow作为错误报告的主要方法。

*** 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对象都是完全瞬态的。也就是说,它是单次使用的(分配给单个读/写操作)。此外,当我从Firebase加载数据时,我创建了一个可变的内容副本。

供参考,这是我创建的保存/加载方法;这存在于我创建的基类中,作为Firebase的瘦包装器,可以加载和保存数据。您可以在这些要点中找到完整的.h.m文件。这些是我与Firebase SDK互动的唯一方式。另请注意,崩溃发生在后台线程上。

- (void)save:(void (^)(BOOL success))completionHandler {
  Firebase *fb = [[Firebase alloc] initWithUrl:self.firebaseURL];
  [fb setValue:[self.contents copy]  withCompletionBlock:^(NSError *error, Firebase *ref) {
    if(completionHandler) {
      completionHandler(error ? NO : YES);
    }
  }];
}

- (void)save {
  [self save:nil];
}

- (void)load:(void (^)(BOOL success))block {
  Firebase *fb = [[Firebase alloc] initWithUrl:self.firebaseURL];
  [fb observeSingleEventOfType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
    _contents = [[snapshot.value isKindOfClass:[NSDictionary class]]?snapshot.value:@{} mutableCopy];
    block(_contents.allKeys.count > 0);
  }];
}

2 个答案:

答案 0 :(得分:3)

编辑:这不再是问题,因为最新的Firebase SDK会在setValue调用中同步克隆您的对象。在将数据传递给Firebase

之前,不再需要手动克隆数据

虽然您正在调用“copy”,但这只是最外层NSDictionary的“浅层”副本,所以如果您在外部NSDictionary中有任何NSDictionaries,并且您正在修改它们,我们仍然可以在Firebase时遇到此错误枚举那些内部NSDictionary对象,并且从callstack中,它看起来好像我们正在枚举其中一个内部对象。

Firebase应该自动为您执行此副本,因此您不必担心它。我们打开了一个错误来解决这个问题。但就目前而言,你需要做一个“深层复制”而不是浅层复制。请参阅此处了解一些可能的方法:deep mutable copy of a NSMutableDictionary(第二或第三个答案看起来很不错)。

答案 1 :(得分:0)

修改: 我相信我找到了例外的潜在原因:

我预感到多个事务试图在同一节点上本地运行并因高堆栈跟踪而导致争用。我最终保存了一组中当前运行的事务,并在启动另一个事件之前测试该节点上正在运行的事务。这是代码:

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

它最初是因为我使用setValue:withCompletionBlock来尝试设置一个包含表示时间戳的数字的节点。它具有各种规则来确定是否可以更新时间戳(如果它&lt; now&lt; now等)。这是我的原始代码:

myValue = @(42);

[myFirebase setValue:myValue withCompletionBlock:^(NSError *error, Firebase *ref) {
    if(!error)
        myMostRecentValue = myValue;
    else
        [myFirebase observeSingleEventOfType:FEventTypeValue withBlock:^(FDataSnapshot *mySnapshot) {
            myMostRecentValue = mySnapshot.value;
        }];
}];

不幸的是,我认为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没有先前的值。因此它返回尝试的值而不是&#34; undefined&#34;占位符如null。我不确定这是一个错误还是一个功能,但需要注意的是它。所以我用以下内容替换了该代码:

[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类别函数,我用它将Firebase中的值复制到我用来缓存最近已知值的本地NSMutableDictionary中(例如在observeSingleEventOfType回调中):

// 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;
}

有时我需要避免viewDidLoad中的往返,所以我将最后一个已知值放在GUI元素中,直到我得到新值的回调。我无法想象这会影响Firebase,但也许是低级别的东西会期待NSDictionary和chokes,因为它引用了我给它的NSMutableDictionary的一部分?

我有点卡住,直到找到解决方案,所以希望这有帮助,谢谢!