编辑:
我相信我找到了异常的潜在原因:
我有一种预感,多个事务试图在同一个节点上本地运行,并由于高堆栈跟踪而导致争用。我最终将当前正在运行的事务保存在一个集合中,并在开始另一个事务之前在节点上测试正在运行的事务。代码如下:
@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 的一部分?
在找到解决方案之前我有点卡住了,所以希望这会有所帮助,谢谢!