You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 07:01:02