iOS12.5.x设备UICollectionView相关NSInternalInconsistencyException崩溃咨询
问题背景
- 崩溃观测来源:Crashlytics平台,暂无法稳定复现,当前正结合日志排查复现路径,崩溃类型为
Fatal Exception: NSInternalInconsistencyException - 核心报错内容:
Fatal Exception: NSInternalInconsistencyException
UICollectionView received layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0xb82e03580e4e92a0> {length = 2, path = 0 - 0}
报错逻辑:UICollectionView接收到了索引路径不存在的单元格的布局属性,触发系统内部一致性断言失败,该崩溃集中发生在iOS 12.5.x系统版本。
- 完整崩溃堆栈:
0 CoreFoundation 0x119180 __exceptionPreprocess 1 libobjc.A.dylib 0x59f8 objc_exception_throw 2 CoreFoundation 0x3288c +[_CFXNotificationTokenRegistration keyCallbacks] 3 Foundation 0x8ab38 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] 4 UIKitCore 0x1e3d80 __45-[UICollectionViewData validateLayoutInRect:]_block_invoke 5 UIKitCore 0x1e3578 -[UICollectionViewData validateLayoutInRect:] 6 UIKitCore 0x1b785c -[UICollectionView layoutSubviews] 7 UIKitCore 0xd54170 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] 8 QuartzCore 0x13ec60 -[CALayer layoutSublayers] 9 QuartzCore 0x143c08 CA::Layer::layout_if_needed(CA::Transaction*) 10 UIKitCore 0xd401a4 -[UIView(Hierarchy) layoutBelowIfNeeded] 11 UIKitCore 0xd468f0 +[UIView(Animation) performWithoutAnimation:] 12 UIKitCore 0xadb814 -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] 13 UIKitCore 0xadbb18 -[UITableView _createPreparedCellForGlobalRow:willDisplay:] 14 UIKitCore 0xaa8294 -[UITableView _updateVisibleCellsNow:isRecursive:] 15 UIKitCore 0xac5640 -[UITableView layoutSubviews] 16 UIKitCore 0xd54170 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] 17 QuartzCore 0x13ec60 -[CALayer layoutSublayers] 18 QuartzCore 0x143c08 CA::Layer::layout_if_needed(CA::Transaction*) 19 QuartzCore 0xa63e4 CA::Context::commit_transaction(CA::Transaction*) 20 QuartzCore 0xd4620 CA::Transaction::commit() 21 QuartzCore 0xd515c CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) 22 CoreFoundation 0xaa4fc __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ 23 CoreFoundation 0xa5224 __CFRunLoopDoObservers 24 CoreFoundation 0xa57a0 __CFRunLoopRun 25 CoreFoundation 0xa4fb4 CFRunLoopRunSpecific 26 GraphicsServices 0xa79c GSEventRunModal 27 UIKitCore 0x8bcc38 UIApplicationMain 28 Project_Name 0x5bf4 main + 25 (AppDelegate.swift:25) 29 libdyld.dylib 0x18e0 start
iOS 12.5.x 版本触发原因
从堆栈链路可以定位到,崩溃发生在UITableView创建待展示cell的无动画布局流程中,触发内层嵌套UICollectionView的布局校验逻辑,具体原因分为两类:
- 系统UIKit缺陷:iOS 12.5.x作为iOS12的最终迭代版本,
-[UICollectionViewData validateLayoutInRect:]方法存在时序校验bug。当UICollectionView嵌套在UITableViewCell内时,外层UITableView的cell复用逻辑和内层UICollectionView的布局更新逻辑如果发生交叉,系统会缓存已经失效的index path对应的布局属性,校验阶段没有做二次有效性判断就直接触发断言,该问题在iOS13及以上版本已被系统修复,因此崩溃仅集中在iOS12.5.x版本。 - 业务代码触发条件:
- 非主线程调用UICollectionView的
reloadData、insertItems、deleteItems等UI更新接口,导致数据源状态和布局状态不同步 - 更新UICollectionView数据源后,未及时清空布局缓存,或在
performBatchUpdatesblock内执行了和实际数据源增删数量不匹配的操作 - 自定义UICollectionViewLayout时,
layoutAttributesForElementsInRect:方法未做有效性过滤,返回了已经从数据源移除的index path对应的布局属性 - 在
performWithoutAnimationblock内执行UICollectionView的批量更新操作,iOS12下无动画更新会跳过布局缓存清理步骤,直接进入校验流程,和堆栈触发链路完全匹配。
- 非主线程调用UICollectionView的
适配修复方案
- 通用基础防护
- 所有UICollectionView、UITableView的UI更新操作强制放在主线程执行,可通过方法交换添加主线程断言,上线前排查完全部非主线程更新UI的问题
- 针对嵌套在UITableViewCell内的UICollectionView,在cell的
prepareForReuse方法中调用UICollectionView的reloadData,reload完成后调用layoutIfNeeded,确保cell复用时内层CollectionView的布局状态和当前数据源完全一致
- iOS12.5.x专项修复
- 主动清理布局缓存:在UICollectionView完成reloadData、所在页面退出、所在cell被复用时,主动调用
collectionView.collectionViewLayout invalidateLayout清空系统缓存的无效布局属性,从根源避免校验到无效属性 - 自定义布局兜底:如果使用自定义UICollectionViewLayout,重写
layoutAttributesForElementsInRect:方法时,对所有待返回的布局属性做index path合法性校验,确认当前数据源中存在对应index path的item,不存在则直接过滤,不返回给系统 - 规避触发场景:避免在
performWithoutAnimationblock内执行UICollectionView的批量更新操作,需要无动画更新时直接调用reloadData后强制布局,不要用performBatchUpdates接口
- 主动清理布局缓存:在UICollectionView完成reloadData、所在页面退出、所在cell被复用时,主动调用
- 复现排查方法
- 使用安装iOS12.5.7系统的测试机,快速上下滑动嵌套CollectionView的TableView列表,同时模拟后台动态更新CollectionView数据源的场景,可稳定复现该崩溃
- 给UICollectionView的
reloadData方法加埋点,记录reload时的数据源总数、当前可见index path,结合崩溃前的日志可快速定位到导致状态不同步的具体业务逻辑。
内容的提问来源于stack exchange,提问作者Dileep Kumar Jaiswal
相关产品推荐
相关产品推荐

