这是一个起初看起来很神奇的问题,大意如下:
- 有一个Table,展示多个消息会话,这些消息会话按最新消息时间排序;
- 某种情况下,新收到一条消息,时间展示为最新,但这条消息没有排在最上方。
因为界面上展示的时间是最新的,所以刚开始遇到这个问题的第一反应是看看数据库里面的时间戳是不是正确的,查看后确认时间是最新的没错。
一时陷入了僵局,因为问题很难重现。
所以梳理了下逻辑:
- 收到新消息,在后台进行处理,执行save动作;
- Core Data保存后发出消息通知变更,主线程使用NSFetchedResultsController和UITableView绑定,收到消息后刷新界面;
- UI界面根据dataSource进行展现,而dataSource根据latestTime进行排序;
因为无法重现,所以先加上了日志输出信息,观察出了发生该现象的时候,主线程都收到两次刷新通知,正常情况下没有。
主线程为什么会发生两次刷新通知呢?
- 主线程内存上发生了变动;
- 其它线程对持久化层做了写动作,通知到主线程。
所以我就在想主线程在内存上发生了什么变动,找了很久但是没找到什么东西。后来同事一语道破天机,打印出changeValues:
<span style="font-size:14px">- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { NSLog(@"didChangeObject %@ from %@ to %@ with %d type, \n change value : %@\n", anObject, indexPath, newIndexPath, type, [anObject changedValues]); }</span>
通过这样的日志信息可以发现主线程在内存中发生了什么变化。
为了验证问题是不是这样引发的,我在一个Demo上进行了模拟和验证(这个Demo是之前一篇博文使用的):
我通过在主线程修改Core Data对象的值(不一定要sortKey),但不保存:
<span style="font-size:14px"> Player *playerObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i]; playerObject.name = [NSString stringWithFormat:@"name-%d", arc4random() % 10000];</span>
接着在其它线程修改sortKey,引发主线程进行刷新:
<span style="font-size:14px">- (void)changeSortKeyInOtherContext:(NSManagedObjectID *)objectId { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] init]; NSPersistentStoreCoordinator *sharedPersistentStoreCoordinator = self.cdViewController.persistentStoreCoordinator; [tmpContext setPersistentStoreCoordinator:sharedPersistentStoreCoordinator]; Player *playerObject = (Player *)[tmpContext objectWithID:objectId]; int age = arc4random() % 100; playerObject.age = @(age); int salary = arc4random() % 10000000; playerObject.salary = @(salary); NSError *error = NULL; if (tmpContext && [tmpContext hasChanges] && ![tmpContext save:&error]) { NSLog(@"Error %@, %@", error, [error localizedDescription]); abort(); } [tmpContext release], tmpContext = nil; }); }</span>
这样就可以模拟出问题场景,进而得到验证。
—— Jason Lee @ Hangzhou