FRC 内容更改时的 UITableView 异常

Posted

技术标签:

【中文标题】FRC 内容更改时的 UITableView 异常【英文标题】:UITableView exception when FRC content changes 【发布时间】:2018-06-09 01:04:51 【问题描述】:

我有一个UITableView,它显示来自NSFetchedResultsController 的数据。 FRC 中显示了不同数量的数据,从一次提取约 20 条记录到最大一次提取约 14k 记录不等。我遇到的问题是,如果我在 tableView 在大提取上滚动时执行提取,我会得到一个异常。当cellForRowAtIndexPath 被调用时,FRC 已经更新并且那里没有数据,导致异常。

我找到了this post,这听起来像是我遇到的情况,但我无法通过该方法解决它。

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
    let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseID, for: indexPath) as! CustomTableViewCell
    // Exception occurs here
    let myObject = fetchedResultsController.object(at: indexPath)
    cell.myObject = myObject

    return cell
  

这是堆栈跟踪:

0   CoreFoundation                      0x0000000110b241e6 __exceptionPreprocess + 294
1   libobjc.A.dylib                     0x000000010b820031 objc_exception_throw + 48
2   CoreData                            0x000000010c2458fd -[NSFetchedResultsController objectAtIndexPath:] + 685
3   MyApp                         0x000000010aaf36c5 _T011MyApp0B14ViewControllerC05tableC0So07UITableC4CellCSo0fC0C_10Foundation9IndexPathV12cellForRowAttF + 437
4   MyApp                         0x000000010aaf39ec _T011MyApp0B14ViewControllerC05tableC0So07UITableC4CellCSo0fC0C_10Foundation9IndexPathV12cellForRowAttFTo + 92
5   UIKit                               0x000000010cf45567 -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 783
6   UIKit                               0x000000010cf45ae4 -[UITableView _createPreparedCellForGlobalRow:willDisplay:] + 74
7   UIKit                               0x000000010cf0ceaa -[UITableView _updateVisibleCellsNow:isRecursive:] + 3168
8   UIKit                               0x000000010cf2d7e0 -[UITableView layoutSubviews] + 176
9   UIKit                               0x000000010ceb77a8 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1515
10  QuartzCore                          0x000000010cc21456 -[CALayer layoutSublayers] + 177
11  QuartzCore                          0x000000010cc25667 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 395
12  QuartzCore                          0x000000010cbac0fb _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 343
13  QuartzCore                          0x000000010cbd979c _ZN2CA11Transaction6commitEv + 568
14  UIKit                               0x000000010cde22ef _UIApplicationFlushRunLoopCATransactionIfTooLate + 167
15  UIKit                               0x000000010d747662 __handleEventQueueInternal + 6875
16  CoreFoundation                      0x0000000110ac6bb1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
17  CoreFoundation                      0x0000000110aab4af __CFRunLoopDoSources0 + 271
18  CoreFoundation                      0x0000000110aaaa6f __CFRunLoopRun + 1263
19  CoreFoundation                      0x0000000110aaa30b CFRunLoopRunSpecific + 635
20  GraphicsServices                    0x0000000115c2fa73 GSEventRunModal + 62
21  UIKit                               0x000000010cde8057 UIApplicationMain + 159
22  MyApp                         0x000000010aacd427 main + 55
23  libdyld.dylib                       0x0000000111c5b955 start + 1

我欢迎有关解决此问题的建议。在tableview 减速完成之前,我想避免做一些像禁用新提取这样的骇人听闻的事情。感谢您的阅读。

更新

作为对评论的回应,这是我对UIFetchedResultsControllerDelegate 的实现。这是 Apple 在documentation 中的代码:

extension ViewController: NSFetchedResultsControllerDelegate 
  func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) 
    tableView.beginUpdates()
  

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                  didChange sectionInfo: NSFetchedResultsSectionInfo,
                  atSectionIndex sectionIndex: Int,
                  for type: NSFetchedResultsChangeType) 
    switch type 
    case .insert:
      tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
    case .delete:
      tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
    case .move:
      break
    case .update:
      break
    
  

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                  didChange anObject: Any,
                  at indexPath: IndexPath?,
                  for type: NSFetchedResultsChangeType,
                  newIndexPath: IndexPath?) 
    switch type 
    case .insert:
      tableView.insertRows(at: [newIndexPath!], with: .fade)
    case .delete:
      tableView.deleteRows(at: [indexPath!], with: .fade)
    case .update:
      tableView.reloadRows(at: [indexPath!], with: .fade)
    case .move:
      tableView.moveRow(at: indexPath!, to: newIndexPath!)
    
  

  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) 
    tableView.endUpdates()
  

新尝试

作为对评论的回应,我添加了我的 NSFetchedResultsControllerDelegate 实现,这是 Apple 在文档中列出的内容。我注意到有强制解包选项,所以我扔了一些guard 声明,我关闭了动画以更好地衡量。我在同一个地方做同样的事情时遇到同样的崩溃。

这是更新后的委托方法,添加了保护语句并关闭了动画:

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                  didChange anObject: Any,
                  at indexPath: IndexPath?,
                  for type: NSFetchedResultsChangeType,
                  newIndexPath: IndexPath?) 

    guard let indexPath = indexPath else  return 

    switch type 
    case .insert:
      guard let newIndexPath = newIndexPath else  return 
      tableView.insertRows(at: [newIndexPath], with: .none)
    case .delete:
      tableView.deleteRows(at: [indexPath], with: .none)
    case .update:
      tableView.reloadRows(at: [indexPath], with: .none)
    case .move:
      guard let newIndexPath = newIndexPath else  return 
      tableView.moveRow(at: indexPath, to: newIndexPath)
    
  

更新 2

作为对评论的回应,这是我用来更新谓词的代码。我有一个startDateendDate 的观察者,它调用updatePredicate()startDateendDate 在按下 segmentedControl 时更新。:

  // This is used to set values for searching via segmentedControl
  @objc dynamic var startDate: Date?
  @objc dynamic var endDate: Date?

  func updatePredicate() 
    // you need a start date or stop executing
    guard let startDate = startDate else  return 

    var predicateArray: [NSPredicate] = []

    if let endDate = endDate 
      let startEndPredicate = NSPredicate(format: "time >= %@ AND time <= %@", argumentArray: [startDate, endDate])
      predicateArray.append(startEndPredicate)
     else 
      // use the startDate's end of day if endDate is nil
      let startPredicate = NSPredicate(format: "time >= %@ AND time <= %@", argumentArray: [startDate, startDate.endOfDay!])
      predicateArray.append(startPredicate)
    

    let sliderPredicate = NSPredicate(format: "magnitude >= %f", argumentArray: [magSlider.value])
    predicateArray.append(sliderPredicate)

    currentPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicateArray)

    fetchResults()
  

这里是 fetchResults():

  func fetchResults() 
    do 
      fetchedResultsController.fetchRequest.fetchBatchSize = 35
      fetchedResultsController.fetchRequest.fetchLimit = 1_000
      try fetchedResultsController.performFetch()
     catch let error 
      print("\(error) \(error.localizedDescription)")
    
    DispatchQueue.main.async 
      self.tableView.reloadData()
      self.tableView.setContentOffset(.zero, animated: true)
      self.updateLabelsForResults()
    
  

更新 3

针对另一条评论,以下是 FRC 声明:

  private lazy var fetchedResultsController: NSFetchedResultsController<EarthquakeEntity> = 
    let managedObjectContext = appDelegate.persistentContainer.viewContext
    let fetchRequest: NSFetchRequest<EarthquakeEntity> = EarthquakeEntity.fetchRequest()
    let dateSortDescriptor = NSSortDescriptor(key: #keyPath(EarthquakeEntity.time), ascending: false)
    fetchRequest.sortDescriptors = [dateSortDescriptor]
    let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: frcKeyPath, cacheName: nil)
    frc.delegate = self

    return frc
  ()

【问题讨论】:

您能否提供如何实现NSFetchedResultsControllerDelegate 方法? @TarasChernyshenko 已更新。看着它,我看到newIndexPath/indexPath 的强制展开,这可能是我的问题。 您能否提供一个用于创建提取请求的代码?您是否在将其更改为 2500 之前设置了获取限制? @DisableR 感谢您的阅读。我用你要求的代码更新了帖子。 fetch中的fetchLimit更改后,你的问题解决了吗? 【参考方案1】:

我最终玩了fetchLimit,这似乎解决了我的问题。

当我执行 fetch 时,我按如下方式使用它:

fetchedResultsController.fetchRequest.fetchLimit = 2_500

2,500 大约是我在不导致应用崩溃的情况下所能获得的最大。

【讨论】:

【参考方案2】:

当 FRC 与fetchBatchSize 一起使用时,FRC 操作的数据集是一个代理数组。如果在访问批处理元素时尚未获取批处理的数据,则在访问时获取。

代理数组的线程安全与您操作的托管对象相同 - 它必须从底层 MOC 的私有队列中访问。您可以查看这篇文章是否是线程安全问题:https://oleb.net/blog/2014/06/core-data-concurrency-debugging/。理想情况下,访问必须以这种方式:

var myObject;
fetchedResultsController.managedObjectContext.performBlockAndWait(
    myObject = fetchedResultsController.object(at: indexPath) 
)
cell.myObject = myObject

另外,在崩溃时请检查这些陈述是否属实:

1) fetchedResultsController.sections.first.numberOfObjects <= 
                                         fetchedResultsController.fetchRequest.fetchLimit

2) fetchedResultsController.sections.first.numberOfObjects == 
                                         tableView.numberOfRows(inSection: 0)

如果 1) 不正确 - FRC 不尊重 fetchLimit,应仅使用 fetchBatchSize

如果 2) 不正确 - 这意味着当 FRC 数据集更改时,tableView 没有得到适当的更新。需要进一步调试才能确定原因。

【讨论】:

以上是关于FRC 内容更改时的 UITableView 异常的主要内容,如果未能解决你的问题,请参考以下文章

使用 FRC Swift 3 更改 CoreData 后 TableView 不更新(reloadData)

调用委托方法后执行选择器

需要帮助设置后台托管对象上下文。在 FRC controllerDidChangeContent 上获取异常

为什么我的UITableView重新加载其所有节标题?

NSFetchedResultsController(和 UITableView)委托方法调用越来越多

我可以使用NSFetchedResultsController来显示不适合UITableView的数据吗?