生产环境Swift UIKit _NativeSet.startIndex.getter崩溃排查求助
生产环境iOS应用崩溃排查求助:_NativeSet.startIndex.getter崩溃点
Crashlytics堆栈追踪
应用: <appName>
平台: Apple
版本: 2.2.14 (30)
问题ID: 6f9f49c30dcc78da2676dcf048076083
会话: 6f812a773f604e83a4c8d9af26a2a63e_DNE_0_v2
时间: 2024年9月9日 周一 19:25:48 GMT+0800(香港标准时间)
| 索引 | 模块 | 内存地址 | 函数/方法 | 偏移量 |
|---|---|---|---|---|
| 0 | <appName> | 0x207910 | specialized _NativeSet.startIndex.getter | +4379932944 |
| 1 | <appName> | 0x1f85f8 | closure #1 in DestinationVC.configureDataSource() | +223 (DestinationVC.swift:223) |
| ... | ... | ... | ... | ... |
| 40 | ??? | 0x1b15c7154 | (Missing) |
关联代码
configureDataSource方法
private func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<DestinationViewCell, AnimalResult.ID> { [weak self] (cell, indexPath, animalID) in guard let self, let thisDestination, let i = self.destVCAnimals.firstIndex (where: { animalResult in animalResult.id == animalID }) else { return } let animal = self.destVCAnimals[i] cell.configureCell(destination: thisDestination, animal: animal) } let sectionHeaderRegistration = UICollectionView.SupplementaryRegistration<SectionHeaderView>(elementKind: SectionHeaderView.ident) { (sectionHeaderView, kind, indexPath) in } let layoutHeaderRegistration = UICollectionView.SupplementaryRegistration<LayoutHeaderView>(elementKind: LayoutHeaderView.ident) { (layoutHeaderView, kind, indexPath) in } dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, animalID -> DestinationViewCell in return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: animalID) } dataSource.supplementaryViewProvider = { [weak self] (collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView? in if kind == "header-view" { // 分区头部 let headerView = collectionView.dequeueConfiguredReusableSupplementary( using: sectionHeaderRegistration, for: indexPath) guard let self else { return headerView } let sectionIdentifiers = dataSource.snapshot().sectionIdentifiers let sectionIconicID = sectionIdentifiers[indexPath.section] guard let animalIDs = iconicTaxons[sectionIconicID] else { return headerView } if let thisDestination, let viewModel { headerView.configure(viewModel, iconicTaxon: (id: sectionIconicID, animals: animalIDs), destination: thisDestination) if indexPath.section == 0, viewModel.userIsInThisDestination(), thisDestination.headerBlurbIsSet() { addTouchView(headerView, indexPath) } } let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSectionHeaderTapGesture(_:))) headerView.addGestureRecognizer(tapGesture) return headerView } else { // 布局头部 let layoutHeaderView = collectionView.dequeueConfiguredReusableSupplementary( using: layoutHeaderRegistration, for: indexPath) guard let self else { return layoutHeaderView } if let thisDestination { layoutHeaderView.configure(destination: thisDestination) } collectionView.collectionViewLayout.invalidateLayout() return layoutHeaderView } } self.configureSnapshot() }
setupUI方法
private func setupUI() { DispatchQueue.main.async { [weak self] in guard let self else { return } let titleLabel = UILabel() let titleLabelText = thisDestination.destinationName ?? thisDestination.myCommunity?.displayName titleLabel.text = titleLabelText titleLabel.numberOfLines = 3 titleLabel.textAlignment = .natural self.navigationItem.titleView = titleLabel self.title = titleLabel.text self.navigationController?.navigationBar.topItem?.largeTitleDisplayMode = .automatic self.navigationController?.navigationBar.topItem?.title = titleLabelText self.navigationController?.navigationBar.topItem?.backButtonDisplayMode = .minimal self.navigationController?.navigationBar.topItem?.style = .browser titleLabel.layoutIfNeeded() self.navigationItem.titleView?.layoutIfNeeded() let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleNavBarTapGesture(_:))) self.navigationController?.navigationBar.addGestureRecognizer(tapGesture) } if #available(iOS 17.0, *) { let styleTrait: UITrait = UITraitUserInterfaceStyle.self print("styleTrait: \(styleTrait)") self.registerForTraitChanges([styleTrait]) { (self: Self, previousTraitCollection: UITraitCollection) in self.configureDataSource() } } }
viewDidLoad方法
override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .systemBackground self.createEmptyView() self.configureCollectionView() self.applyConstraints() self.setupUI() self.configureDataSource() self.watch() }
排查与修复建议
1. 修复集合访问越界问题
在supplementaryViewProvider中访问sectionIdentifiers[indexPath.section]时未做边界检查,当collectionView的section数量与snapshot的section数量不一致时,会触发越界访问,进而导致底层_NativeSet集合崩溃。添加边界校验:
let sectionIdentifiers = dataSource.snapshot().sectionIdentifiers // 添加边界检查 guard indexPath.section < sectionIdentifiers.count else { return headerView } let sectionIconicID = sectionIdentifiers[indexPath.section]
2. 解决闭包与手势的强引用/僵尸对象问题
- 手势识别器的强引用:为头部视图和导航栏添加的
UITapGestureRecognizer会强引用VC对象,如果VC被释放后手势仍存在,会触发僵尸对象访问。在VC的deinit方法中移除相关手势:
deinit { // 移除头部视图的手势 collectionView?.visibleSupplementaryViews(ofKind: "header-view").forEach { view in view.gestureRecognizers?.forEach { $0.removeTarget(self, action: nil) } } // 移除导航栏的手势 navigationController?.navigationBar.gestureRecognizers?.removeAll { guard let action = $0.action else { return false } return NSStringFromSelector(action) == NSStringFromSelector(#selector(handleNavBarTapGesture(_:))) } }
- 闭包中潜在的已释放对象访问:在cellRegistration闭包中,
destVCAnimals如果依赖于thisDestination,当VC释放后该集合可能已被销毁,建议将destVCAnimals改为可选类型,或在访问前确保其有效性:
guard let self, let thisDestination, let animals = self.destVCAnimals, // 改为可选后先解包 let i = animals.firstIndex(where: { $0.id == animalID }) else { return } let animal = animals[i]
3. 避免重复重建DataSource
在iOS17的 trait 变化回调中直接调用configureDataSource()会重复创建数据源,旧的数据源闭包可能仍被collectionView持有,导致新旧数据源交替时的集合访问异常。改为更新快照而非重建数据源:
self.registerForTraitChanges([styleTrait]) { (self: Self, previousTraitCollection: UITraitCollection) in // 仅更新快照刷新UI,不重建数据源 self.configureSnapshot() self.collectionView.reloadData() }
4. 优化异步闭包中的self引用
setupUI中的DispatchQueue.main.async闭包内,导航栏手势的target直接引用self,建议配合deinit中的手势移除逻辑,避免潜在的循环引用。
内容的提问来源于stack exchange,提问作者Ryan Ashton
相关产品推荐
相关产品推荐

