Nightmare with performBatchUpdates crash

一个人想着一个人 提交于 2020-01-01 04:31:08

问题


I am facing a nightmare of a crash during performBatchUpdates on a collection view.

The problem is basically this: I have a lot of images on a directory on a server. I want to show the thumbnails of those files on a collection view. But the thumbnail have to be downloaded from the server asynchronously. As they arrive they will be inserted on the collection view using something like this:

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];
             });

the problem is this (I think). Suppose that at time = 0, the collection view has 10 items. I then add 100 more files to the server. The application sees the new files and start downloading the thumbnails. As the thumbnails download they will be inserted on the collection view. But because the downloads can take different times and this download operation is asynchronous, at one point iOS will lost track of how many elements the collection has and the whole thing will crash with this catastrophic infamous message.

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (213) must be equal to the number of items contained in that section before the update (154), plus or minus the number of items inserted or deleted from that section (40 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'

The proof I have something fishy is going on is that if I print the count of items on the data set I see exactly 213. So, the dataset matches the correct number and the message is nonsense.

I have had this problem before, here but that was an iOS 7 project. Somehow the problem returned now on iOS 8 and the solutions there are not working and now the dataset IS IN SYNC.


回答1:


I think the problem is caused by the indexes.

Key:

  • For updated and deleted items, the indexes have to be the indexes of original items.
  • For inserted items, the indexes have to be the indexes of final items.

Here is a demo code with comments:

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
    }
}



回答2:


It sounds like you need to do a bit extra work with batching which images have appeared for each animation group. From dealing with crashes like this before, the way performBatchUpdates works is

  1. Before invoking your block, it double checks all the item counts and saves them by calling numberOfItemsInSection (this is the 154 in your error message).
  2. It runs the block, tracking the inserts/deletes, and calculates what the final number of items should be based on the insertions and deletions.
  3. After the block is run, it double checks the counts it calculated to the actual counts when it asks your dataSource numberOfItemsInSection (this is the 213 number). If it doesn't match, it will crash.

Based on your variables insertedIndexes and changedIndexes, you're pre-calculating which things need to show up based on the download response from server, and then running the batch. However I'm guessing your numberOfItemsInSection method is always just returning the 'true' count of items.

So if a download completes during step 2, when it performs the sanity check in '3', your numbers won't line up anymore.

Easiest solution: Wait until all files have downloaded, then do a single batchUpdates. Probably not the best user experience but it avoids this issue.

Harder solution: Perform batches as needed, and track which items have already shown up / are currently animating separately from the total number of items. Something like:

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;
               }];
             });
}

The only thing to solve after that would be to make sure you run one final check for leftover items if the last item to download finished during an animation (maybe have a method performFinalAnimationIfNeeded that you run in the completion block)




回答3:


For anyone having a similar issue, let me quote the documentation on UICollectionView:

If the collection view's layout is not up to date before you call this method, a reload may occur. To avoid problems, you should update your data model inside the updates block or ensure the layout is updated before you call performBatchUpdates(_:completion:).

I was originally referencing an array of a separate model object, but decided to keep a local copy of the array within the view controller and update the array within performBatchUpdates(_:completion:).

Problem was solved.




回答4:


This may be happening because you do need to also make sure with collectionViews to delete and insert sections. when you try to insert an item in a section that doesn't exist you will get this crash.

Preform Batch updates doesn't know that you meant to add section X+1 when you insert an item at X+1, X. without you already having added that section in.



来源:https://stackoverflow.com/questions/37846653/nightmare-with-performbatchupdates-crash

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!