多个子 ManagedObjectContexts 和 NSManagedObjectContextDidSaveNotification 罕见崩溃
Rare crash with multiple child ManagedObjectContexts and NSManagedObjectContextDidSaveNotification
我们从正式版应用程序中获取了崩溃报告。它崩溃了大约 1% 的应用程序打开,我们永远无法让应用程序在 XCode 会话期间或在模拟器上崩溃。但是我们能够在没有 XCode 会话 运行 的设备上重现崩溃。我相信这是多个 ManagedObjectContext
之间的竞争条件,它们试图通过 NSManagedObjectContextDidSaveNotification
获取有关更改的信息
但首先这里是一个示例崩溃报告:
Exception Type: SIGSEGV
Exception Codes: SEGV_ACCERR at 0x6000000c
Crashed Thread: 22
Thread 0:
0 libsystem_kernel.dylib 0x30ba24c4 semaphore_wait_trap + 8
1 libdispatch.dylib 0x30ad17ff _dispatch_barrier_sync_f_slow + 363
2 CoreData 0x22452ced _perform + 173
3 CoreData 0x2245fd9f -[NSManagedObjectContext(_NestedContextSupport) managedObjectContextDidRegisterObjectsWithIDs:] + 67
4 CoreData 0x223e3467 _PFFaultHandlerLookupRow + 1319
5 CoreData 0x223e2bd1 _PF_FulfillDeferredFault + 233
6 CoreData 0x223e2a35 _sharedIMPL_pvfk_core + 61
7 myApp 0x0014bc11 -[E5ServiceEndpointController serviceEndpointUrlForKey:withHost:andContext:] (E5ServiceEndpointController.m:116)
8 myApp 0x0014b3bd -[E5ServiceEndpointController urlForKey:] (E5ServiceEndpointController.m:45)
9 myApp 0x0014968b -[E5NotificationController fetchMessages] (E5NotificationController.m:117)
10 myApp 0x0013bec1 __41-[E5MenuPointController fetchMenuPoints:]_block_invoke (E5MenuPointController.m:172)
11 myApp 0x002af435 __66-[RKObjectRequestOperation setCompletionBlockWithSuccess:failure:]_block_invoke244 (RKObjectRequestOperation.m:474)
12 libdispatch.dylib 0x30aca2e3 _dispatch_call_block_and_release + 11
13 libdispatch.dylib 0x30aca2cf _dispatch_client_callout + 23
14 libdispatch.dylib 0x30acdd2f _dispatch_main_queue_callback_4CF + 1331
15 CoreFoundation 0x2268f619 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
16 CoreFoundation 0x2268dd19 __CFRunLoopRun + 1513
17 CoreFoundation 0x225db3b1 CFRunLoopRunSpecific + 477
18 CoreFoundation 0x225db1c3 CFRunLoopRunInMode + 107
19 GraphicsServices 0x29c08201 GSEventRunModal + 137
20 UIKit 0x25c4543d UIApplicationMain + 1441
21 myApp 0x00151125 main (main.m:14)
22 libdyld.dylib 0x30aebaaf start + 3
Thread 12:
0 libsystem_kernel.dylib 0x30ba24c4 semaphore_wait_trap + 8
1 libdispatch.dylib 0x30ad17ff _dispatch_barrier_sync_f_slow + 363
2 CoreData 0x22452ced _perform + 173
3 CoreData 0x2245f991 -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:] + 241
4 CoreData 0x223d11df -[NSManagedObjectContext executeFetchRequest:error:] + 595
5 myApp 0x0014baf5 -[E5ServiceEndpointController serviceEndpointUrlForKey:withHost:andContext:] (E5ServiceEndpointController.m:108)
6 myApp 0x0014b33d -[E5ServiceEndpointController pathForKey:] (E5ServiceEndpointController.m:37)
7 myApp 0x001ac9c7 -[E5MainViewController crashMeThreadOne] (E5MainViewController.m:357)
8 Foundation 0x233fe68b __NSThread__main__ + 1119
9 libsystem_pthread.dylib 0x30c32e23 _pthread_body + 139
10 libsystem_pthread.dylib 0x30c32d97 _pthread_start + 119
11 libsystem_pthread.dylib 0x30c30b20 thread_start + 8
Thread 22 Crashed:
0 libobjc.A.dylib 0x3056bf46 objc_msgSend + 6
1 CoreFoundation 0x22681e31 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 13
2 CoreFoundation 0x225dd6cd _CFXNotificationPost + 1785
3 Foundation 0x23333dd9 -[NSNotificationCenter postNotificationName:object:userInfo:] + 73
4 CoreData 0x2240dbf7 -[NSManagedObjectContext(_NSInternalAdditions) _didSaveChanges] + 2303
5 CoreData 0x223f412f -[NSManagedObjectContext save:] + 1299
6 myApp 0x0024774f __61-[NSManagedObjectContext(RKAdditions) saveToPersistentStore:]_block_invoke16 (NSManagedObjectContext+RKAdditions.m:65)
7 CoreData 0x2245780d developerSubmittedBlockToNSManagedObjectContextPerform + 181
8 libdispatch.dylib 0x30aca2cf _dispatch_client_callout + 23
9 libdispatch.dylib 0x30ad186b _dispatch_barrier_sync_f_slow + 471
10 CoreData 0x224579a7 -[NSManagedObjectContext performBlockAndWait:] + 183
11 myApp 0x0024720f -[NSManagedObjectContext(RKAdditions) saveToPersistentStore:] (NSManagedObjectContext+RKAdditions.m:64)
12 myApp 0x0013d9a9 __51-[E5MenuPointController updateNotificationCounters]_block_invoke (E5MenuPointController.m:299)
13 Foundation 0x233e8db1 __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ + 9
14 Foundation 0x23353e4d -[NSBlockOperation main] + 149
15 Foundation 0x233467c7 -[__NSOperationInternal _start:] + 775
16 Foundation 0x233eb71b __NSOQSchedule_f + 187
17 libdispatch.dylib 0x30ad2729 _dispatch_queue_drain + 1469
18 libdispatch.dylib 0x30accaad _dispatch_queue_invoke + 85
19 libdispatch.dylib 0x30ad3f9f _dispatch_root_queue_drain + 395
20 libdispatch.dylib 0x30ad53c3 _dispatch_worker_thread3 + 95
21 libsystem_pthread.dylib 0x30c30dc1 _pthread_wqthread + 669
22 libsystem_pthread.dylib 0x30c30b14 start_wqthread + 8
所有崩溃报告的共同点是涉及 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__
,并且一些其他线程尝试使用它们自己的 ManagedObjectContext (MOC),例如同时在托管对象上执行提取或更改数据。
我们的应用程序使用 RestKit 0.24 进行 CoreData 管理和子 MOC 创建。我们使用 RestKit 方法
-(NSManagedObjectContext*)newChildManagedObjectContextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType tracksChanges:(BOOL)tracksChanges
为每个线程创建一个新的子 MOC。
这个方法比较简单,可以查看here on gitHub
在那个 gitHub 代码中你甚至可以看到 tracksChanges
使用以下代码添加了一个观察者 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleManagedObjectContextDidSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:observedContext];
并且还删除了 dealloc
[=29 中的观察者=]
我们的发现是,如果我们将 tracksChanges
设置为 NO
,则不会发生崩溃。如果 tracksChanges
设置为 YES
,我们可以重现崩溃。请记住,崩溃不会每次都发生。这种情况非常罕见,我们更改了代码以不断重新运行有问题的代码片段,以便有机会重现崩溃。
这是 E5ServiceEndpointController
class 的一段代码,如果 tracksChanges
设置为 YES
:
则可能会导致崩溃
- (NSString *)urlForKey:(NSString *)key
{
NSManagedObjectContext *serviceEndpointContext = [self newChildManagedObjectContextForServiceEndpoints];
return [self serviceEndpointUrlForKey:key withHost:YES andContext:serviceEndpointContext];
}
- (NSString *)pathForKey:(NSString *)key
{
NSManagedObjectContext *serviceEndpointContext = [self newChildManagedObjectContextForServiceEndpoints];
return [self serviceEndpointUrlForKey:key withHost:NO andContext:serviceEndpointContext];
}
-(NSManagedObjectContext *)newChildManagedObjectContextForServiceEndpoints{
return [[[E5RestKitManager sharedInstance] managedObjectStore] newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:YES];
}
- (NSString *)serviceEndpointUrlForKey:(NSString *)key withHost:(BOOL)includeHost andContext:(NSManagedObjectContext *)context
{
NSFetchRequest *serviceEndpointFetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"ServiceEndpoint"];
[serviceEndpointFetchRequest setPredicate:[NSPredicate predicateWithFormat:@"key = %@", key]];
NSArray *result = [context executeFetchRequest:serviceEndpointFetchRequest error:nil];
// more code here
}
我们在这里错过了什么?我们需要用什么来保护 executeFetchRequest
吗?如果我们检测到 NSManagedObjectContextDidSaveNotification
,我们是否需要手动更新我们的子 MOC 或放弃所有操作?我们对架构有误解吗?
看起来像是不遵循队列限制规则的简单案例:
urlForKey
和 pathForKey
调用 newChildManagedObjectContextForServiceEndpoints
获取新的托管对象上下文
newChildManagedObjectContextForServiceEndpoints
使用 NSPrivateQueueConcurrencyType
创建此上下文。
urlForKey
和 pathForKey
将它们的上下文传递给 serviceEndpointUrlForKey:andContext:
serviceEndpointUrlForKey:andContext:
这样做:
NSArray *result = [context executeFetchRequest:serviceEndpointFetchRequest error:nil];
规则是:如果您使用队列限制(此处为NSPrivateQueueConcurrencyType
)创建托管对象上下文,您必须使用performBlock:
或performBlockAndWait:
使用该上下文时。如果不这样做,您将绕过队列限制应该提供的并发支持。您需要在使用这些上下文之一的任何地方修复它。您还应该研究使用 com.apple.CoreData.ConcurrencyDebug
来查找与并发相关的错误。
我们从正式版应用程序中获取了崩溃报告。它崩溃了大约 1% 的应用程序打开,我们永远无法让应用程序在 XCode 会话期间或在模拟器上崩溃。但是我们能够在没有 XCode 会话 运行 的设备上重现崩溃。我相信这是多个 ManagedObjectContext
之间的竞争条件,它们试图通过 NSManagedObjectContextDidSaveNotification
但首先这里是一个示例崩溃报告:
Exception Type: SIGSEGV
Exception Codes: SEGV_ACCERR at 0x6000000c
Crashed Thread: 22
Thread 0:
0 libsystem_kernel.dylib 0x30ba24c4 semaphore_wait_trap + 8
1 libdispatch.dylib 0x30ad17ff _dispatch_barrier_sync_f_slow + 363
2 CoreData 0x22452ced _perform + 173
3 CoreData 0x2245fd9f -[NSManagedObjectContext(_NestedContextSupport) managedObjectContextDidRegisterObjectsWithIDs:] + 67
4 CoreData 0x223e3467 _PFFaultHandlerLookupRow + 1319
5 CoreData 0x223e2bd1 _PF_FulfillDeferredFault + 233
6 CoreData 0x223e2a35 _sharedIMPL_pvfk_core + 61
7 myApp 0x0014bc11 -[E5ServiceEndpointController serviceEndpointUrlForKey:withHost:andContext:] (E5ServiceEndpointController.m:116)
8 myApp 0x0014b3bd -[E5ServiceEndpointController urlForKey:] (E5ServiceEndpointController.m:45)
9 myApp 0x0014968b -[E5NotificationController fetchMessages] (E5NotificationController.m:117)
10 myApp 0x0013bec1 __41-[E5MenuPointController fetchMenuPoints:]_block_invoke (E5MenuPointController.m:172)
11 myApp 0x002af435 __66-[RKObjectRequestOperation setCompletionBlockWithSuccess:failure:]_block_invoke244 (RKObjectRequestOperation.m:474)
12 libdispatch.dylib 0x30aca2e3 _dispatch_call_block_and_release + 11
13 libdispatch.dylib 0x30aca2cf _dispatch_client_callout + 23
14 libdispatch.dylib 0x30acdd2f _dispatch_main_queue_callback_4CF + 1331
15 CoreFoundation 0x2268f619 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
16 CoreFoundation 0x2268dd19 __CFRunLoopRun + 1513
17 CoreFoundation 0x225db3b1 CFRunLoopRunSpecific + 477
18 CoreFoundation 0x225db1c3 CFRunLoopRunInMode + 107
19 GraphicsServices 0x29c08201 GSEventRunModal + 137
20 UIKit 0x25c4543d UIApplicationMain + 1441
21 myApp 0x00151125 main (main.m:14)
22 libdyld.dylib 0x30aebaaf start + 3
Thread 12:
0 libsystem_kernel.dylib 0x30ba24c4 semaphore_wait_trap + 8
1 libdispatch.dylib 0x30ad17ff _dispatch_barrier_sync_f_slow + 363
2 CoreData 0x22452ced _perform + 173
3 CoreData 0x2245f991 -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:] + 241
4 CoreData 0x223d11df -[NSManagedObjectContext executeFetchRequest:error:] + 595
5 myApp 0x0014baf5 -[E5ServiceEndpointController serviceEndpointUrlForKey:withHost:andContext:] (E5ServiceEndpointController.m:108)
6 myApp 0x0014b33d -[E5ServiceEndpointController pathForKey:] (E5ServiceEndpointController.m:37)
7 myApp 0x001ac9c7 -[E5MainViewController crashMeThreadOne] (E5MainViewController.m:357)
8 Foundation 0x233fe68b __NSThread__main__ + 1119
9 libsystem_pthread.dylib 0x30c32e23 _pthread_body + 139
10 libsystem_pthread.dylib 0x30c32d97 _pthread_start + 119
11 libsystem_pthread.dylib 0x30c30b20 thread_start + 8
Thread 22 Crashed:
0 libobjc.A.dylib 0x3056bf46 objc_msgSend + 6
1 CoreFoundation 0x22681e31 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 13
2 CoreFoundation 0x225dd6cd _CFXNotificationPost + 1785
3 Foundation 0x23333dd9 -[NSNotificationCenter postNotificationName:object:userInfo:] + 73
4 CoreData 0x2240dbf7 -[NSManagedObjectContext(_NSInternalAdditions) _didSaveChanges] + 2303
5 CoreData 0x223f412f -[NSManagedObjectContext save:] + 1299
6 myApp 0x0024774f __61-[NSManagedObjectContext(RKAdditions) saveToPersistentStore:]_block_invoke16 (NSManagedObjectContext+RKAdditions.m:65)
7 CoreData 0x2245780d developerSubmittedBlockToNSManagedObjectContextPerform + 181
8 libdispatch.dylib 0x30aca2cf _dispatch_client_callout + 23
9 libdispatch.dylib 0x30ad186b _dispatch_barrier_sync_f_slow + 471
10 CoreData 0x224579a7 -[NSManagedObjectContext performBlockAndWait:] + 183
11 myApp 0x0024720f -[NSManagedObjectContext(RKAdditions) saveToPersistentStore:] (NSManagedObjectContext+RKAdditions.m:64)
12 myApp 0x0013d9a9 __51-[E5MenuPointController updateNotificationCounters]_block_invoke (E5MenuPointController.m:299)
13 Foundation 0x233e8db1 __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ + 9
14 Foundation 0x23353e4d -[NSBlockOperation main] + 149
15 Foundation 0x233467c7 -[__NSOperationInternal _start:] + 775
16 Foundation 0x233eb71b __NSOQSchedule_f + 187
17 libdispatch.dylib 0x30ad2729 _dispatch_queue_drain + 1469
18 libdispatch.dylib 0x30accaad _dispatch_queue_invoke + 85
19 libdispatch.dylib 0x30ad3f9f _dispatch_root_queue_drain + 395
20 libdispatch.dylib 0x30ad53c3 _dispatch_worker_thread3 + 95
21 libsystem_pthread.dylib 0x30c30dc1 _pthread_wqthread + 669
22 libsystem_pthread.dylib 0x30c30b14 start_wqthread + 8
所有崩溃报告的共同点是涉及 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__
,并且一些其他线程尝试使用它们自己的 ManagedObjectContext (MOC),例如同时在托管对象上执行提取或更改数据。
我们的应用程序使用 RestKit 0.24 进行 CoreData 管理和子 MOC 创建。我们使用 RestKit 方法
-(NSManagedObjectContext*)newChildManagedObjectContextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType tracksChanges:(BOOL)tracksChanges
为每个线程创建一个新的子 MOC。
这个方法比较简单,可以查看here on gitHub
在那个 gitHub 代码中你甚至可以看到 tracksChanges
使用以下代码添加了一个观察者 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleManagedObjectContextDidSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:observedContext];
并且还删除了 dealloc
[=29 中的观察者=]
我们的发现是,如果我们将 tracksChanges
设置为 NO
,则不会发生崩溃。如果 tracksChanges
设置为 YES
,我们可以重现崩溃。请记住,崩溃不会每次都发生。这种情况非常罕见,我们更改了代码以不断重新运行有问题的代码片段,以便有机会重现崩溃。
这是 E5ServiceEndpointController
class 的一段代码,如果 tracksChanges
设置为 YES
:
- (NSString *)urlForKey:(NSString *)key
{
NSManagedObjectContext *serviceEndpointContext = [self newChildManagedObjectContextForServiceEndpoints];
return [self serviceEndpointUrlForKey:key withHost:YES andContext:serviceEndpointContext];
}
- (NSString *)pathForKey:(NSString *)key
{
NSManagedObjectContext *serviceEndpointContext = [self newChildManagedObjectContextForServiceEndpoints];
return [self serviceEndpointUrlForKey:key withHost:NO andContext:serviceEndpointContext];
}
-(NSManagedObjectContext *)newChildManagedObjectContextForServiceEndpoints{
return [[[E5RestKitManager sharedInstance] managedObjectStore] newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:YES];
}
- (NSString *)serviceEndpointUrlForKey:(NSString *)key withHost:(BOOL)includeHost andContext:(NSManagedObjectContext *)context
{
NSFetchRequest *serviceEndpointFetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"ServiceEndpoint"];
[serviceEndpointFetchRequest setPredicate:[NSPredicate predicateWithFormat:@"key = %@", key]];
NSArray *result = [context executeFetchRequest:serviceEndpointFetchRequest error:nil];
// more code here
}
我们在这里错过了什么?我们需要用什么来保护 executeFetchRequest
吗?如果我们检测到 NSManagedObjectContextDidSaveNotification
,我们是否需要手动更新我们的子 MOC 或放弃所有操作?我们对架构有误解吗?
看起来像是不遵循队列限制规则的简单案例:
urlForKey
和pathForKey
调用newChildManagedObjectContextForServiceEndpoints
获取新的托管对象上下文newChildManagedObjectContextForServiceEndpoints
使用NSPrivateQueueConcurrencyType
创建此上下文。urlForKey
和pathForKey
将它们的上下文传递给serviceEndpointUrlForKey:andContext:
serviceEndpointUrlForKey:andContext:
这样做:NSArray *result = [context executeFetchRequest:serviceEndpointFetchRequest error:nil];
规则是:如果您使用队列限制(此处为NSPrivateQueueConcurrencyType
)创建托管对象上下文,您必须使用performBlock:
或performBlockAndWait:
使用该上下文时。如果不这样做,您将绕过队列限制应该提供的并发支持。您需要在使用这些上下文之一的任何地方修复它。您还应该研究使用 com.apple.CoreData.ConcurrencyDebug
来查找与并发相关的错误。