【问题标题】:Nightmare with performBatchUpdates crashperformBatchUpdates 的噩梦崩溃
【发布时间】:2016-06-15 22:10:44
【问题描述】:

我在performBatchUpdatescollection view 上面临着一场噩梦。

问题基本上是这样的:我在服务器上的目录中有很多图像。我想在collection view 上显示这些文件的缩略图。但缩略图必须从服务器异步下载。当它们到达时,它们将使用以下方式插入到集合视图中:

dispatch_async(dispatch_get_main_queue(),
             ^{
               [self.collectionView performBatchUpdates:^{

                 if (removedIndexes && [removedIndexes count] > 0) {
                   [self.collectionView deleteItemsAtIndexPaths:removedIndexes];
                 }

                 if (changedIndexes && [changedIndexes count] > 0) {
                   [self.collectionView reloadItemsAtIndexPaths:changedIndexes];
                 }

                 if (insertedIndexes && [insertedIndexes count] > 0) {
                   [self.collectionView insertItemsAtIndexPaths:insertedIndexes];
                 }

               } completion:nil];
             });

问题是这个(我认为)。假设在时间 = 0 时,集合视图有 10 个项目。然后我再向服务器添加 100 个文件。应用程序看到新文件并开始下载缩略图。随着缩略图的下载,它们将被插入到集合视图中。但是因为下载可能需要不同的时间,而且这个下载操作是asynchronous,iOS 会在某一时刻失去对集合有多少元素的跟踪,并且整个事情都会因为这个灾难性的臭名昭著的消息而崩溃。

*** 由于未捕获的异常“NSInternalInconsistencyException”而终止应用程序,原因:“无效更新:无效 第 0 节中的项目数。包含在第 0 节中的项目数 更新后的现有节(213)必须等于 更新前该部分中包含的项目 (154),加号或减号 从该部分插入或删除的项目数(40 已插入,0 已删除)并加上或减去移入的项目数 或移出该部分(0 移入,0 移出)。'

我有一些可疑之处的证据是,如果我打印数据集上的项目数,我会看到准确的 213。因此,数据集匹配正确的数字并且消息是无意义的。

我以前遇到过这个问题,here 但那是一个 iOS 7 项目。不知何故,现在在 iOS 8 上又出现了问题,并且那里的解决方案不起作用,现在数据集 IS IN SYNC

【问题讨论】:

    标签: ios iphone uicollectionview performbatchupdates


    【解决方案1】:

    听起来您需要做一些额外的工作来批处理每个动画group 出现的图像。从以前处理这样的崩溃来看,performBatchUpdates 的工作方式是

    1. 在调用您的块之前,它会再次检查所有项目计数并通过调用 numberOfItemsInSection 保存它们(这是您的错误消息中的 154)。
    2. 它运行块,跟踪插入/删除,并根据插入和删除计算应该的最终项目数。
    3. 块运行后,当它询问您的数据源numberOfItemsInSection(这是 213 数字)时,它会仔细检查计算的计数与实际计数。如果不匹配,则会崩溃。

    根据您的变量insertedIndexeschangedIndexes,您正在根据来自服务器的下载响应预先计算需要显示哪些内容,然后运行批处理。但是我猜你的 numberOfItemsInSection 方法总是只返回“真实”的项目数。

    因此,如果在第 2 步中完成下载,当它在“3”中执行完整性检查时,您的号码将不再排列。

    最简单的解决方案:等到所有文件都下载完毕,然后执行单个batchUpdates。可能不是最好的用户体验,但它避免了这个问题。

    更难的解决方案:根据需要执行批处理,并从项目总数中单独跟踪哪些项目已经显示/当前正在制作动画。类似的东西:

    BOOL _performingAnimation;
    NSInteger _finalItemCount;
    
    - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
        return _finalItemCount;
    }
    
    - (void)somethingDidFinishDownloading {
        if (_performingAnimation) {
            return;
        }
        // Calculate changes.
        dispatch_async(dispatch_get_main_queue(),
                 ^{
                    _performingAnimation = YES;
                   [self.collectionView performBatchUpdates:^{
    
                     if (removedIndexes && [removedIndexes count] > 0) {
                       [self.collectionView deleteItemsAtIndexPaths:removedIndexes];
                     }
    
                     if (changedIndexes && [changedIndexes count] > 0) {
                       [self.collectionView reloadItemsAtIndexPaths:changedIndexes];
                     }
    
                     if (insertedIndexes && [insertedIndexes count] > 0) {
                       [self.collectionView insertItemsAtIndexPaths:insertedIndexes];
                     }
    
                     _finalItemCount += (insertedIndexes.count - removedIndexes.count);
                   } completion:^{
                     _performingAnimation = NO;
                   }];
                 });
    }
    

    在那之后唯一要解决的问题是,如果最后一个要下载的项目在动画期间完成,请确保对剩余项目进行最后一次检查(可能有一个方法 performFinalAnimationIfNeeded 在完成块中运行)

    【讨论】:

      【解决方案2】:

      我认为问题是由索引引起的。

      键:

      • 对于更新和删除的项目,索引必须是原始项目的索引。
      • 对于插入的项目,索引必须是最终项目的索引。

      这是一个带有 cmets 的演示代码:

      class CollectionViewController: UICollectionViewController {
      
          var items: [String]!
      
          let before = ["To Be Deleted 1", "To Be Updated 1", "To Be Updated 2", "To Be Deleted 2", "Stay"]
          let after = ["Updated 1", "Updated 2", "Added 1", "Stay", "Added 2"]
      
          override func viewDidLoad() {
              super.viewDidLoad()
      
              self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Refresh", style: .Plain, target: self, action: #selector(CollectionViewController.onRefresh(_:)))
      
              items = before
          }
      
          func onRefresh(_: AnyObject) {
      
              items = after
      
              collectionView?.performBatchUpdates({
                  self.collectionView?.deleteItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 3, inSection: 0), ])
      
                  // Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete and reload the same index path
                  // self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 1, inSection: 0), ])
      
                  // NOTE: Have to be the indexes of original list
                  self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 1, inSection: 0), NSIndexPath(forRow: 2, inSection: 0), ])
      
                  // Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert item 4 into section 0, but there are only 4 items in section 0 after the update'
                  // self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0), NSIndexPath(forRow: 5, inSection: 0), ])
      
                  // NOTE: Have to be index of final list
                  self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 2, inSection: 0), NSIndexPath(forRow: 4, inSection: 0), ])
      
              }, completion: nil)
          }
      
          override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
              return 1
          }
          override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
              return items.count
          }
      
          override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
              let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath)
      
              let label = cell.viewWithTag(100) as! UILabel
      
              label.text = items[indexPath.row]
      
              return cell
          }
      }
      

      【讨论】:

        【解决方案3】:

        对于任何有类似问题的人,让我引用UICollectionView 上的文档:

        如果在调用此方法之前集合视图的布局不是最新的,则可能会发生重新加载。为避免出现问题,您应该在更新块内更新您的数据模型或确保在调用 performBatchUpdates(_:completion:) 之前更新布局。

        我最初引用了一个单独模型对象的数组,但决定在视图控制器中保留该数组的本地副本,并在 performBatchUpdates(_:completion:) 中更新该数组。

        问题解决了。

        【讨论】:

          【解决方案4】:

          这可能会发生,因为您还需要确保使用 collectionViews 删除和插入部分。当您尝试在不存在的部分中插入项目时,您将遇到此崩溃。

          当您在 X+1、X 处插入项目时,Preform Batch 更新不知道您打算添加第 X+1 节。而您还没有添加该节。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2010-12-21
            • 2020-03-14
            • 2010-11-13
            • 2010-12-22
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多