【问题标题】:Firebase FSnapshotUtilities crashes due to mutating a NSMutableDictionary while enumeratingFirebase FSnapshotUtilities 由于在枚举时改变 NSMutableDictionary 而崩溃
【发布时间】:2014-04-27 16:49:38
【问题描述】:

编辑根据接受的答案,解决方案是使用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);
  }];
}

【问题讨论】:

  • 我的错,我删除了我的东西。我看错了你的三元运算符。
  • @Logan 混乱可能是我写了一段相当长的逻辑的错——抱歉;)
  • 嗯,这当然很奇怪。它看起来确实像堆栈中的 Firebase 问题,但我不确定为什么我们在枚举时会发生变异。那不应该发生。我明天去看看,然后回复你。
  • ping @MichaelLehenbauer ...运气好吗?
  • @ZaneClaes 抱歉回复慢! :-( 再看一遍,再仔细想想,我怀疑我知道是怎么回事了。看看我刚刚提交的答案。

标签: ios objective-c firebase


【解决方案1】:

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

虽然您调用的是“复制”,但这只是最外层 NSDictionary 的“浅层”副本,因此如果您在外部 NSDictionary 中有任何 NSDictionaries,并且您正在修改它们,当 Firebase 时我们仍然会遇到此错误枚举那些内部 NSDictionary 对象,并且从调用堆栈中,看起来我们正在枚举内部对象之一。

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

【讨论】:

  • 这听起来像是一个可能的答案,但我需要用周末的压力测试来确认。
  • 这并没有解决问题。这是它发生的屏幕截图:cl.ly/image/3P0l2O2g3O0C
  • 以下是我的 Firebase 实施的更新要点:gist.github.com/zaneclaes/566af446a52dc007a43e
  • 这是一个屏幕截图,显示了所有线程在做什么。请注意,我的代码没有运行。至少有 2 个 Firebase 线程处于活动状态,但我的线程都没有处于枚举/变异过程中,因此 Firebase 中的冲突似乎是 100%(与我对对象的操作无关):cl.ly/image/2u3W222v2H2O跨度>
  • 我也尝试过使用[Firebase setDispatchQueue:dispatch_get_main_queue()];。这并没有解决问题; FirebaseWorker 线程似乎仍然是在后台线程上创建的......
【解决方案2】:

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

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

@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 来尝试设置一个包含表示时间戳的数字的节点。它有各种规则来确定时间戳是否可以更新(如果它是

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 没有先前的值可以回退。因此它返回尝试的值,而不是像 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 和窒息,因为它引用了我给它的 NSMutableDictionary 的一部分?

在找到解决方案之前我有点卡住了,所以希望这会有所帮助,谢谢!

【讨论】:

  • FWIW,mutableDeepCopy 可以大大简化:return (NSMutableDictionary *)CFBridgingRelease(CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (CFDictionaryRef)self, kCFPropertyListMutableContainers));
  • 是的,谢谢你,在你的建议之后,我偶然发现了这个cocoanetics.com/2009/09/deep-copying-dictionaries,并实现了一个copyDeep、mutableCopyDeep和immutableCopyDeep,它们分别使用archivedDataWithRootObject、kCFPropertyListMutableContainersAndLeaves和kCFPropertyListImmutable。这样我就可以控制是否保留或覆盖节点可变性。酷:-)
猜你喜欢
  • 2011-07-13
  • 1970-01-01
  • 2013-01-21
  • 2011-03-12
  • 2017-08-15
  • 2011-11-09
  • 2014-07-04
  • 1970-01-01
相关资源
最近更新 更多