基于CoreData的聊天APP崩溃:scrollToItem触发无效索引路径异常
解决CoreData驱动IM应用中scrollCollectionToBottom的随机崩溃问题
我来帮你分析并解决这个随机崩溃问题——我之前在CoreData驱动的列表场景里也遇到过类似的坑。
问题回顾
你开发的基于CoreData的即时通讯应用,在聊天控制器的scrollCollectionToBottom方法中出现随机崩溃,抛出异常:
exception 'NSInvalidArgumentException', reason:'attempt to scroll to invalid indexPath
相关代码如下:
func scrollCollectionToBottom(animated: Bool){ if self.fetchedResultsControler.sections?.count == 0 { return } if self.fetchedResultsControler.sections![self.fetchedResultsControler.sections!.count - 1].numberOfObjects == 0{ return } let lastSection = self.fetchedResultsControler.sections!.count - 1 let lastItem = self.fetchedResultsControler.sections![lastSection].numberOfObjects - 1 let indexPath = IndexPath.init(item: lastItem, section: lastSection) if animated{ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { self.collectionViewMessages.scrollToItem(at: indexPath, at: .bottom, animated: true) }) return } if group.type == GroupType.privat.rawValue{ constrHeightCollViewSubj.constant = 0 self.view.layoutIfNeeded() } DispatchQueue.main.async(){ self.collectionViewMessages.reloadData() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { self.collectionViewMessages.scrollToItem(at: indexPath, at: .bottom, animated: false) }) } }
崩溃根源分析
这个随机崩溃的核心问题是你提前计算的indexPath在异步延迟执行期间,CoreData的数据已经发生了变化,具体来说:
- 异步延迟的时间窗口风险:你用了
0.5秒的延迟来执行滚动,在这短短半秒内,完全可能发生这些情况:最后一条消息被删除、整个聊天会话的消息被清空,或者Fetched Results Controller(FRC)完成了新一轮的数据同步,导致你之前计算的section/item数量已经过时。 - reloadData后的失效:在非动画分支中,你先调用
reloadData()刷新列表,然后延迟滚动。reloadData会让CollectionView重新从FRC拉取最新数据,如果这时候数据已经变化,之前缓存的indexPath对应的条目可能已经不存在了。 - 强制解包的潜在风险:虽然你前面做了sections的空判断,但判断和后续强制解包
!之间,FRC可能已经完成了数据更新,不过这次崩溃的直接触发点还是无效的indexPath。
修复方案
要解决这个问题,核心思路是在执行滚动操作的瞬间,基于最新的FRC数据重新计算有效indexPath,而不是提前计算并缓存。
方案1:在异步闭包内重新计算indexPath
把indexPath的计算逻辑移到延迟执行的闭包内部,确保每次滚动时都用最新的数据:
func scrollCollectionToBottom(animated: Bool){ if animated{ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { // 闭包内重新获取最新的section数据 guard let sections = self.fetchedResultsControler.sections, !sections.isEmpty else { return } let lastSection = sections.count - 1 let lastSectionInfo = sections[lastSection] guard lastSectionInfo.numberOfObjects > 0 else { return } let lastItem = lastSectionInfo.numberOfObjects - 1 let indexPath = IndexPath(item: lastItem, section: lastSection) // 额外验证indexPath是否在CollectionView的有效范围内 if self.collectionViewMessages.dataSource?.collectionView( self.collectionViewMessages, numberOfItemsInSection: lastSection ) ?? 0 > lastItem { self.collectionViewMessages.scrollToItem(at: indexPath, at: .bottom, animated: true) } }) return } if group.type == GroupType.privat.rawValue{ constrHeightCollViewSubj.constant = 0 self.view.layoutIfNeeded() } DispatchQueue.main.async(){ self.collectionViewMessages.reloadData() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { // 同样在闭包内重新计算最新的indexPath guard let sections = self.fetchedResultsControler.sections, !sections.isEmpty else { return } let lastSection = sections.count - 1 let lastSectionInfo = sections[lastSection] guard lastSectionInfo.numberOfObjects > 0 else { return } let lastItem = lastSectionInfo.numberOfObjects - 1 let indexPath = IndexPath(item: lastItem, section: lastSection) if self.collectionViewMessages.dataSource?.collectionView( self.collectionViewMessages, numberOfItemsInSection: lastSection ) ?? 0 > lastItem { self.collectionViewMessages.scrollToItem(at: indexPath, at: .bottom, animated: false) } }) } }
方案2:优化触发时机,移除不必要的延迟
如果0.5秒的延迟是为了等待Cell加载或UI布局完成,可以考虑更可靠的触发方式:
- 监听CollectionView的
UICollectionViewDelegateFlowLayout的collectionView(_:layout:sizeForItemAt:),或者在viewDidLayoutSubviews中判断是否需要滚动 - 如果你需要每次FRC数据更新后自动滚动到底部,可以在FRC的
controllerDidChangeContent(_:)代理方法中处理滚动逻辑,这样能保证数据更新完成后立即执行滚动,不需要延迟。
方案3:添加通用的indexPath有效性检查
可以封装一个辅助方法,在调用scrollToItem前验证indexPath是否有效:
// 验证indexPath是否有效的辅助方法 private func isIndexPathValid(_ indexPath: IndexPath) -> Bool { guard let sections = fetchedResultsControler.sections else { return false } guard indexPath.section < sections.count else { return false } let sectionInfo = sections[indexPath.section] return indexPath.item < sectionInfo.numberOfObjects }
然后在滚动前调用这个方法:
if isIndexPathValid(indexPath) { self.collectionViewMessages.scrollToItem(at: indexPath, at: .bottom, animated: animated) }
内容的提问来源于stack exchange,提问作者Denis Windover
相关产品推荐
相关产品推荐

