多个子 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 或放弃所有操作?我们对架构有误解吗?

看起来像是不遵循队列限制规则的简单案例:

  1. urlForKeypathForKey 调用 newChildManagedObjectContextForServiceEndpoints 获取新的托管对象上下文
  2. newChildManagedObjectContextForServiceEndpoints 使用 NSPrivateQueueConcurrencyType 创建此上下文。
  3. urlForKeypathForKey 将它们的上下文传递给 serviceEndpointUrlForKey:andContext:
  4. serviceEndpointUrlForKey:andContext: 这样做:

    NSArray *result = [context executeFetchRequest:serviceEndpointFetchRequest error:nil];
    

规则是:如果您使用队列限制(此处为NSPrivateQueueConcurrencyType)创建托管对象上下文,您必须使用performBlock:performBlockAndWait: 使用该上下文时。如果不这样做,您将绕过队列限制应该提供的并发支持。您需要在使用这些上下文之一的任何地方修复它。您还应该研究使用 com.apple.CoreData.ConcurrencyDebug 来查找与并发相关的错误。