在performBatchUpdates更新UICollectionView时发生崩溃求助
UICollectionView performBatchUpdates 崩溃排查与解决
崩溃堆栈信息
Last Exception Backtrace: 0 CoreFoundation 0x1d2bbde88 __exceptionPreprocess + 164 (NSException.m:202) 1 libobjc.A.dylib 0x1cbef78d8 objc_exception_throw + 60 (objc-exception.mm:356) 2 Foundation 0x1cd4e5b4c -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 188 (NSException.m:242) 3 UIKitCore 0x1d5082998 -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:collectionViewAnimator:] + 3964 (UICollectionView.m:9492) 4 UIKitCore 0x1d4fe17bc -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:animator:animationHandler:] + 520 (UICollectionView.m:11824) 5 FieldService 0x1006f2f0c specialized EditLatestNewsListViewController.controllerDidChangeContent(_:) + 276 (EditLatestNewsListViewController.swift:170) 6 FieldService 0x1006f1920 EditLatestNewsListViewController.controllerDidChangeContent(_:) + 4 (:0) 7 FieldService 0x1006f1920 @objc EditLatestNewsListViewController.controllerDidChangeContent(_:) + 48 8 CoreData 0x1da3f0c04 __82-[NSFetchedResultsController(PrivateMethods) _core_managedObjectContextDidChange:]_block_invoke + 5576 (NSFetchedResultsController.m:2329) 9 CoreData 0x1da4147e8 developerSubmittedBlockToNSManagedObjectContextPerform + 156 (NSManagedObjectContext.m:3964) 10 CoreData 0x1da414338 -[NSManagedObjectContext performBlockAndWait:] + 208 (NSManagedObjectContext.m:4075) 11 CoreData 0x1da3f2c98 -[NSFetchedResultsController _core_managedObjectContextDidChange:] + 124 (NSFetchedResultsController.m:2363) 12 CoreFoundation 0x1d2beb404 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 148 (CFNotificationCenter.c:661) 13 CoreFoundation 0x1d2c92474 ___CFXRegistrationPost_block_invoke + 88 (CFNotificationCenter.c:175) 14 CoreFoundation 0x1d2c75724 _CFXRegistrationPost + 440 (CFNotificationCenter.c:201) 15 CoreFoundation 0x1d2bffa08 _CFXNotificationPost + 704 (CFNotificationCenter.c:1193) 16 Foundation 0x1ccffcffc -[NSNotificationCenter postNotificationName:object:userInfo:] + 92 (NSNotification.m:518) 17 CoreData 0x1da40cd70 -[NSManagedObjectContext _createAndPostChangeNotification:deletions:updates:refreshes:deferrals:wasMerge:] + 1796 (NSManagedObjectContext.m:8631) 18 CoreData 0x1da4214bc -[NSManagedObjectContext _processRecentChanges:] + 2508 (NSManagedObjectContext.m:7722) 19 CoreData 0x1da40c518 -[NSManagedObjectContext _mergeChangesFromDidSaveDictionary:usingObjectIDs:] + 3576 (NSManagedObjectContext.m:3701) 20 CoreData 0x1da4154d8 -[NSManagedObjectContext mergeChangesFromContextDidSaveNotification:] + 376 (NSManagedObjectContext.m:3730) 21 MagicalRecord 0x10237f228 +[NSManagedObjectContext(MagicalRecord) rootContextDidSave:] + 448 (NSManagedObjectContext+MagicalRecord.m:236) 22 libdispatch.dylib 0x1da18c4b4 _dispatch_call_block_and_release + 32 (init.c:1518) 23 libdispatch.dylib 0x1da18dfdc _dispatch_client_callout + 20 (object.m:560) 24 libdispatch.dylib 0x1da19c7f4 _dispatch_main_queue_drain + 928 (inline_internal.h:2632) 25 libdispatch.dylib 0x1da19c444 _dispatch_main_queue_callback_4CF + 44 (queue.c:7887) 26 CoreFoundation 0x1d2c4e6f8 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16 (CFRunLoop.c:1780) 27 CoreFoundation 0x1d2c30058 __CFRunLoopRun + 2036 (CFRunLoop.c:3147) 28 CoreFoundation 0x1d2c34ed4 CFRunLoopRunSpecific + 612 (CFRunLoop.c:3418) 29 GraphicsServices 0x20bf36368 GSEventRunModal + 164 (GSEvent.c:2196) 30 UIKitCore 0x1d51133d0 -[UIApplication _run] + 888 (UIApplication.m:3745) 31 UIKitCore 0x1d5113034 UIApplicationMain + 340 (UIApplication.m:5335) 32 FieldService 0x1005c3214 main + 68 (AppDelegate.swift:20) 33 dyld 0x1f12a0960 start + 2528 (dyldMain.cpp:1170)
原实现代码
private var collectionViewChanges = [[NSFetchedResultsChangeType: [IndexPath]]]() func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { collectionView?.performBatchUpdates({ [weak self] in if let collectionViewChanges = self?.collectionViewChanges { for collectionViewChange in collectionViewChanges { for (type, indexPaths) in collectionViewChange { switch type { case .insert: self?.collectionView?.insertItems(at: indexPaths) case .delete: self?.collectionView?.deleteItems(at: indexPaths) case .update: self?.collectionView?.reloadItems(at: indexPaths) case .move: if let indexPath = indexPaths.first, let newIndexPath = indexPaths.last { self?.collectionView?.moveItem(at: indexPath, to: newIndexPath) } } } } } delegate?.contentDidChange() updateView() }) }
崩溃原因分析
- 变更存储结构错误:
[[NSFetchedResultsChangeType: [IndexPath]]]的嵌套结构会导致同类型操作被拆分,且无法保证正确的执行顺序。UICollectionView批量更新要求按删除→插入→移动→更新的顺序执行,否则会出现索引路径不匹配,触发断言失败。 - Move操作处理不当:将Move的旧、新路径存入同一个数组的方式不可靠,无法确保
first是旧路径、last是新路径,容易导致移动操作的索引错误。 - 非UI操作侵入批量更新闭包:
delegate?.contentDidChange()和updateView()不属于批量更新的UI操作,放在performBatchUpdates的闭包中会干扰UI状态的一致性。 - 变更记录未清空:未在处理完变更后清空
collectionViewChanges,导致后续变更时重复执行旧操作,引发索引混乱。
解决方案
1. 重构变更存储结构
替换原有的嵌套字典数组,改用分类型的变量存储不同类型的变更:
private var insertedIndexPaths = [IndexPath]() private var deletedIndexPaths = [IndexPath]() private var updatedIndexPaths = [IndexPath]() private var movedIndexPaths = [(from: IndexPath, to: IndexPath)]()
2. 正确记录FRC变更
在controller(_:didChange:at:for:newIndexPath:)代理方法中分类记录变更:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case .insert: if let newIndexPath = newIndexPath { insertedIndexPaths.append(newIndexPath) } case .delete: if let indexPath = indexPath { deletedIndexPaths.append(indexPath) } case .update: if let indexPath = indexPath { updatedIndexPaths.append(indexPath) } case .move: if let indexPath = indexPath, let newIndexPath = newIndexPath { movedIndexPaths.append((from: indexPath, to: newIndexPath)) } @unknown default: break } }
3. 修正批量更新逻辑
按正确顺序执行变更,并将非UI操作移到completion闭包中,同时清空变更记录:
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) { collectionView?.performBatchUpdates({ [weak self] in guard let self = self else { return } // 严格按顺序执行:先删除,再插入,然后移动,最后更新 if !self.deletedIndexPaths.isEmpty { self.collectionView?.deleteItems(at: self.deletedIndexPaths) } if !self.insertedIndexPaths.isEmpty { self.collectionView?.insertItems(at: self.insertedIndexPaths) } self.movedIndexPaths.forEach { move in self.collectionView?.moveItem(at: move.from, to: move.to) } if !self.updatedIndexPaths.isEmpty { self.collectionView?.reloadItems(at: self.updatedIndexPaths) } }, completion: { [weak self] _ in // 批量更新完成后再执行后续操作 self?.delegate?.contentDidChange() self?.updateView() // 清空变更记录,避免重复执行 self?.insertedIndexPaths.removeAll() self?.deletedIndexPaths.removeAll() self?.updatedIndexPaths.removeAll() self?.movedIndexPaths.removeAll() }) }
额外注意事项
- 确保FRC的所有代理方法都在主线程执行,UICollectionView的UI操作必须在主线程完成,使用MagicalRecord时需注意上下文通知合并的线程。
- 避免在批量更新过程中调用
reloadData(),这会打断动画并可能引发崩溃。 - 若存在多section场景,需确保IndexPath的section值正确,同时处理FRC的section变更事件(若用到)。
内容的提问来源于stack exchange,提问作者kunass2
相关产品推荐
相关产品推荐

