CRASH 尝试删除并重新加载相同的索引路径

CRASH attempt to delete and reload the same index path

CollectionViewController.m line 439 __50-[CollectionViewController photoLibraryDidChange:]_block_invoke

致命异常:NSInternalInconsistencyException 尝试删除并重新加载相同的索引路径 ({length = 2, path = 0 - 26007})

- (void)photoLibraryDidChange:(PHChange *)changeInstance
{
    // Call might come on any background queue. Re-dispatch to the main queue to handle it.
    dispatch_async(dispatch_get_main_queue(), ^{

        // check if there are changes to the assets (insertions, deletions, updates)
        PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
        if (collectionChanges) {

            // get the new fetch result
            self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];

            UICollectionView *collectionView = self.collectionView;

            if (![collectionChanges hasIncrementalChanges] || [collectionChanges hasMoves]) {
                // we need to reload all if the incremental diffs are not available
                [collectionView reloadData];

            } else {
                // if we have incremental diffs, tell the collection view to animate insertions and deletions
                [collectionView performBatchUpdates:^{
                    NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
                    if ([removedIndexes count]) {
                        [collectionView deleteItemsAtIndexPaths:[removedIndexes aapl_indexPathsFromIndexesWithSection:0]];
                    }
                    NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
                    if ([insertedIndexes count]) {
                        [collectionView insertItemsAtIndexPaths:[insertedIndexes aapl_indexPathsFromIndexesWithSection:0]];
                    }
                    NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
                    if ([changedIndexes count]) {
                        [collectionView reloadItemsAtIndexPaths:[changedIndexes aapl_indexPathsFromIndexesWithSection:0]];
                    }
                } completion:NULL];
            }

            [self resetCachedAssets];
        }
    });
}

来源:https://developer.apple.com/devcenter/download.action?path=/wwdc_2014/wwdc_2014_sample_code/exampleappusingphotosframework.zip

我无法重现这个问题。可能是什么问题呢?非常感谢!

我今天能够重现这个。为此,您需要:

  1. 打开正在侦听更改的应用程序
  2. 打开照片应用,将一组照片从 iCloud 共享相册保存到您的照片库
  3. 转到照片应用程序,删除其中一些照片
  4. 再次转到 iCloud 共享相册并再次保存您删除的一些照片。你会看到这种情况发生。

我发现了一个更新的代码,似乎可以更好地处理此处的更新行为: https://developer.apple.com/library/ios/documentation/Photos/Reference/PHPhotoLibraryChangeObserver_Protocol/

但它仍然无法处理这种情况,也无法处理要删除的索引更大的情况(即由于未捕获的异常而终止应用程序 'NSInternalInconsistencyException',原因:'attempt to delete item 9 from section 0 which only contains 9 items before the update')。我创建了这个代码的更新版本,可以更好地处理这个问题,到目前为止我还没有崩溃。

func photoLibraryDidChange(changeInfo: PHChange!) {

    // Photos may call this method on a background queue;
    // switch to the main queue to update the UI.
    dispatch_async(dispatch_get_main_queue()) {


        // Check for changes to the list of assets (insertions, deletions, moves, or updates).
        if let collectionChanges = changeInfo.changeDetailsForFetchResult(self.assetsFetchResult) {

            // Get the new fetch result for future change tracking.
            self.assetsFetchResult = collectionChanges.fetchResultAfterChanges

            if collectionChanges.hasIncrementalChanges {

                // Get the changes as lists of index paths for updating the UI.
                var removedPaths: [NSIndexPath]?
                var insertedPaths: [NSIndexPath]?
                var changedPaths: [NSIndexPath]?
                if let removed = collectionChanges.removedIndexes {
                    removedPaths = self.indexPathsFromIndexSetWithSection(removed,section: 0)
                }
                if let inserted = collectionChanges.insertedIndexes {
                    insertedPaths = self.indexPathsFromIndexSetWithSection(inserted,section: 0)
                }
                if let changed = collectionChanges.changedIndexes {
                    changedPaths = self.indexPathsFromIndexSetWithSection(changed,section: 0)
                }
                var shouldReload = false
                if changedPaths != nil && removedPaths != nil{
                    for changedPath in changedPaths!{
                        if contains(removedPaths!,changedPath){
                            shouldReload = true
                            break
                        }
                    }

                }

                if removedPaths?.last?.item >= self.assetsFetchResult.count{
                    shouldReload = true
                }

                if shouldReload{
                    self.collectionView.reloadData()
                }else{
                    // Tell the collection view to animate insertions/deletions/moves
                    // and to refresh any cells that have changed content.
                    self.collectionView.performBatchUpdates(
                        {
                            if let theRemovedPaths = removedPaths {
                                self.collectionView.deleteItemsAtIndexPaths(theRemovedPaths)
                            }
                            if let theInsertedPaths = insertedPaths {
                                self.collectionView.insertItemsAtIndexPaths(theInsertedPaths)
                            }
                            if let theChangedPaths = changedPaths{
                                self.collectionView.reloadItemsAtIndexPaths(theChangedPaths)
                            }
                            if (collectionChanges.hasMoves) {
                                collectionChanges.enumerateMovesWithBlock() { fromIndex, toIndex in
                                    let fromIndexPath = NSIndexPath(forItem: fromIndex, inSection: 0)
                                    let toIndexPath = NSIndexPath(forItem: toIndex, inSection: 0)
                                    self.collectionView.moveItemAtIndexPath(fromIndexPath, toIndexPath: toIndexPath)
                                }
                            }
                        }, completion: nil)

                }

            } else {
                // Detailed change information is not available;
                // repopulate the UI from the current fetch result.
                self.collectionView.reloadData()
            }
        }
    }
}

func indexPathsFromIndexSetWithSection(indexSet:NSIndexSet?,section:Int) -> [NSIndexPath]?{
    if indexSet == nil{
        return nil
    }
    var indexPaths:[NSIndexPath] = []

    indexSet?.enumerateIndexesUsingBlock { (index, Bool) -> Void in
        indexPaths.append(NSIndexPath(forItem: index, inSection: section))
    }
    return indexPaths

}

Swift3 / iOS10版本:

func photoLibraryDidChange(_ changeInstance: PHChange) {
    guard let collectionView = self.collectionView else {
        return
    }

    // Photos may call this method on a background queue;
    // switch to the main queue to update the UI.
    DispatchQueue.main.async {
        guard let fetchResults = self.fetchResults else {
            collectionView.reloadData()
            return
        }

        // Check for changes to the list of assets (insertions, deletions, moves, or updates).
        if let collectionChanges = changeInstance.changeDetails(for: fetchResults) {
            // Get the new fetch result for future change tracking.
            self.fetchResults = collectionChanges.fetchResultAfterChanges

            if collectionChanges.hasIncrementalChanges {
                // Get the changes as lists of index paths for updating the UI.
                var removedPaths: [IndexPath]?
                var insertedPaths: [IndexPath]?
                var changedPaths: [IndexPath]?
                if let removed = collectionChanges.removedIndexes {
                    removedPaths = self.indexPaths(from: removed, section: 0)
                }
                if let inserted = collectionChanges.insertedIndexes {
                    insertedPaths = self.indexPaths(from:inserted, section: 0)
                }
                if let changed = collectionChanges.changedIndexes {
                    changedPaths = self.indexPaths(from: changed, section: 0)
                }
                var shouldReload = false
                if let removedPaths = removedPaths, let changedPaths = changedPaths {
                    for changedPath in changedPaths {
                        if removedPaths.contains(changedPath) {
                            shouldReload = true
                            break
                        }
                    }
                }

                if let item = removedPaths?.last?.item {
                    if item >= fetchResults.count {
                        shouldReload = true
                    }
                }

                if shouldReload {
                    collectionView.reloadData()
                } else {
                    // Tell the collection view to animate insertions/deletions/moves
                    // and to refresh any cells that have changed content.
                    collectionView.performBatchUpdates({
                        if let theRemovedPaths = removedPaths {
                            collectionView.deleteItems(at: theRemovedPaths)
                        }
                        if let theInsertedPaths = insertedPaths {
                            collectionView.insertItems(at: theInsertedPaths)
                        }
                        if let theChangedPaths = changedPaths {
                            collectionView.reloadItems(at: theChangedPaths)
                        }

                        collectionChanges.enumerateMoves { fromIndex, toIndex in
                            collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0),
                                                    to: IndexPath(item: toIndex, section: 0))
                        }
                    })
                }
            } else {
                // Detailed change information is not available;
                // repopulate the UI from the current fetch result.
                collectionView.reloadData()
            }
        }
    }
}

func indexPaths(from indexSet: IndexSet?, section: Int) -> [IndexPath]? {
    guard let set = indexSet else {
        return nil
    }

    return set.map { (index) -> IndexPath in
        return IndexPath(item: index, section: section)
    }
}

我在Objective-C中实现了中的代码。

- (void)photoLibraryDidChange:(PHChange *)changeInstance {

    dispatch_async(dispatch_get_main_queue(), ^{

        PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
        if (collectionChanges) {

            self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];

            UICollectionView *collectionView = self.collectionView;
            NSArray *removedPaths;
            NSArray *insertedPaths;
            NSArray *changedPaths;

            if ([collectionChanges hasIncrementalChanges]) {
                NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
                removedPaths = [self indexPathsFromIndexSet:removedIndexes withSection:0];

                NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
                insertedPaths = [self indexPathsFromIndexSet:insertedIndexes withSection:0];

                NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
                changedPaths = [self indexPathsFromIndexSet:changedIndexes withSection:0];

                BOOL shouldReload = NO;

                if (changedPaths != nil && removedPaths != nil) {
                    for (NSIndexPath *changedPath in changedPaths) {
                        if ([removedPaths containsObject:changedPath]) {
                            shouldReload = YES;
                            break;
                        }
                    }
                }

                if (removedPaths.lastObject && ((NSIndexPath *)removedPaths.lastObject).item >= self.assetsFetchResults.count) {
                    shouldReload = YES;
                }

                if (shouldReload) {
                    [collectionView reloadData];

                } else {
                    [collectionView performBatchUpdates:^{
                        if (removedPaths) {
                            [collectionView deleteItemsAtIndexPaths:removedPaths];
                        }

                        if (insertedPaths) {
                            [collectionView insertItemsAtIndexPaths:insertedPaths];
                        }

                        if (changedPaths) {
                            [collectionView reloadItemsAtIndexPaths:changedPaths];
                        }

                        if ([collectionChanges hasMoves]) {
                            [collectionChanges enumerateMovesWithBlock:^(NSUInteger fromIndex, NSUInteger toIndex) {
                                NSIndexPath *fromIndexPath = [NSIndexPath indexPathForItem:fromIndex inSection:0];
                                NSIndexPath *toIndexPath = [NSIndexPath indexPathForItem:toIndex inSection:0];
                                [collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath];
                            }];
                        }

                    } completion:NULL];
                }

                [self resetCachedAssets];
            } else {
                [collectionView reloadData];
            }
        }
    });
}

- (NSArray *)indexPathsFromIndexSet:(NSIndexSet *)indexSet withSection:(int)section {
    if (indexSet == nil) {
        return nil;
    }
    NSMutableArray *indexPaths = [[NSMutableArray alloc] init];

    [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
        [indexPaths addObject:[NSIndexPath indexPathForItem:idx inSection:section]];
    }];

    return indexPaths;
}

这是对@batkru 答案的改进,不再需要变量 shouldReload:

func photoLibraryDidChange(changeInstance: PHChange) {
    dispatch_async(dispatch_get_main_queue(), {
        let changeDetails = changeInstance.changeDetailsForFetchResult(self.assetsFetchResult)

        if let details = changeDetails {
            self.assetsFetchResult = details.fetchResultAfterChanges

            if details.hasIncrementalChanges {
                var removedIndexes: [NSIndexPath]?
                var insertedIndexes: [NSIndexPath]?
                var changedIndexes: [NSIndexPath]?

                if let removed = details.removedIndexes {
                    removedIndexes = createIndexPathsFromIndices(removed)
                }
                if let inserted = details.insertedIndexes {
                    insertedIndexes = createIndexPathsFromIndices(inserted)
                }
                if let changed = details.changedIndexes {
                    changedIndexes = createIndexPathsFromIndices(changed)
                }

                if removedIndexes != nil && changedIndexes != nil {
                    for removedIndex in removedIndexes! {
                        let indexOfAppearanceOfRemovedIndexInChangedIndexes = find(changedIndexes!, removedIndex)
                        if let index = indexOfAppearanceOfRemovedIndexInChangedIndexes {
                            changedIndexes!.removeAtIndex(index)
                        }
                    }
                }

                self.collectionView?.performBatchUpdates({
                    if let removed = removedIndexes {
                        self.collectionView?.deleteItemsAtIndexPaths(removed)
                    }
                    if let inserted = insertedIndexes {
                        self.collectionView?.insertItemsAtIndexPaths(inserted)
                    }
                    if let changed = changedIndexes {
                        self.collectionView?.reloadItemsAtIndexPaths(changed)
                    }
                    if details.hasMoves {
                        changeDetails!.enumerateMovesWithBlock({ fromIndex, toIndex in
                            self.collectionView?.moveItemAtIndexPath(NSIndexPath(forItem: fromIndex, inSection: 0), toIndexPath: NSIndexPath(forItem: toIndex, inSection: 0))
                        })
                    }
                }, completion: nil)
            } else {
                self.collectionView?.reloadData()
            }
        }
    })
}

所以我对@FernandoEscher 对@batkryu 的解决方案的翻译做得很好,除了最近重新连接了具有大量更改的 iCloud 照片库的情况。在这种情况下,集合变得完全没有响应并可能崩溃。核心问题是 photoLibraryDidChange 将在 performBatchUpdates 完成触发之前再次调用。在 performBatchUpdates 完成之前调用 performBatchUpdates 似乎会降低性能。我怀疑发生崩溃是因为 assetsFetchResults 被修改,而动画是 运行 之前的值。

太棒了,这是我所做的:

初始化的其他地方....

self.phPhotoLibChageMutex = dispatch_semaphore_create(1);

_

- (void)photoLibraryDidChange:(PHChange *)changeInstance {
    dispatch_semaphore_wait(self.phPhotoLibChageMutex, DISPATCH_TIME_FOREVER);

    dispatch_async(dispatch_get_main_queue(), ^{

        PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
        if (collectionChanges) {

            self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];

            UICollectionView *collectionView = self.collectionView;
            NSArray *removedPaths;
            NSArray *insertedPaths;
            NSArray *changedPaths;

            if ([collectionChanges hasIncrementalChanges]) {
                NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
                removedPaths = [self indexPathsFromIndexSet:removedIndexes withSection:0];

                NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
                insertedPaths = [self indexPathsFromIndexSet:insertedIndexes withSection:0];

                NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
                changedPaths = [self indexPathsFromIndexSet:changedIndexes withSection:0];

                BOOL shouldReload = NO;

                if (changedPaths != nil && removedPaths != nil) {
                    for (NSIndexPath *changedPath in changedPaths) {
                        if ([removedPaths containsObject:changedPath]) {
                            shouldReload = YES;
                            break;
                        }
                    }
                }

                if (removedPaths.lastObject && ((NSIndexPath *)removedPaths.lastObject).item >= self.assetsFetchResults.count) {
                    shouldReload = YES;
                }

                if (shouldReload) {
                    [collectionView reloadData];
                    [self fixupSelection];
                    dispatch_semaphore_signal(self.phPhotoLibChageMutex);
                } else {
                    [collectionView performBatchUpdates:^{
                        if (removedPaths) {
                            [collectionView deleteItemsAtIndexPaths:removedPaths];
                        }

                        if (insertedPaths) {
                            [collectionView insertItemsAtIndexPaths:insertedPaths];
                        }

                        if (changedPaths) {
                            [collectionView reloadItemsAtIndexPaths:changedPaths];
                        }

                        if ([collectionChanges hasMoves]) {
                            [collectionChanges enumerateMovesWithBlock:^(NSUInteger fromIndex, NSUInteger toIndex) {
                                NSIndexPath *fromIndexPath = [NSIndexPath indexPathForItem:fromIndex inSection:0];
                                NSIndexPath *toIndexPath = [NSIndexPath indexPathForItem:toIndex inSection:0];
                                [collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath];
                            }];
                        }

                    } completion:^(BOOL finished) {
                        [self fixupSelection];
                        dispatch_semaphore_signal(self.phPhotoLibChageMutex);
                    }];
                }

                [self resetCachedAssets];
            } else {
                [collectionView reloadData];
                [self fixupSelection];
                dispatch_semaphore_signal(self.phPhotoLibChageMutex);
            }
        }else{
            dispatch_semaphore_signal(self.phPhotoLibChageMutex);
        }
    });
}

- (NSArray *)indexPathsFromIndexSet:(NSIndexSet *)indexSet withSection:(int)section {
    if (indexSet == nil) {
        return nil;
    }
    NSMutableArray *indexPaths = [[NSMutableArray alloc] init];

    [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
        [indexPaths addObject:[NSIndexPath indexPathForItem:idx inSection:section]];
    }];

    return indexPaths;
}

我刚刚在批量更新完成后移动了reloadItemsAtIndexPaths以修复同时删除和重新加载的崩溃。

来自 PHFetchResultChangeDetailschangedIndexes 的文档:

These indexes are relative to the original fetch result (the fetchResultBeforeChanges property) after you’ve applied the changes described by the removedIndexes and insertedIndexes properties; when updating your app’s interface, apply changes after removals and insertions and before moves.

PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
[collectionView performBatchUpdates:^{ 
        NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
        if ([removedIndexes count]) {
            [collectionView deleteItemsAtIndexPaths:[self indexPathsFromIndexes:removedIndexes withSection:0]];
        }
        NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
        if ([insertedIndexes count]) {
            [collectionView insertItemsAtIndexPaths:[self indexPathsFromIndexes:insertedIndexes withSection:0]];
        }
    } completion:^(BOOL finished) {
        if (finished) {
            // Puting this after removes and inserts indexes fixes a crash of deleting and reloading at the same time.
            // From docs: When updating your app’s interface, apply changes after removals and insertions and before moves.
            NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
            if ([changedIndexes count]) {
                [collectionView reloadItemsAtIndexPaths:[self indexPathsFromIndexes:changedIndexes withSection:0]];
            }
       }
    }

终于阅读了文档(不是来自 developer.apple.com,而是来自 Xcode),应用增量更改的正确方法是:

collectionView.performBatchUpdates({
    // removedIndexes
    // insertedIndexes
}, completion: {
    // changedIndexes (using collectionView.cellForItem(at: index))
    collectionView.performBatchUpdates({
        // enumerateMoves
    })
})

在实施此方法之前,我们在移动项目时发生了随机崩溃。

为什么这是正确的?

"removedIndexes" 和 "insertedIndexes" 先走,因为这个索引是相对于原始获取结果

"removedIndexes" - "索引是相对于原始抓取结果的(fetchResultBeforeChanges 属性);更新应用程序界面时,应用在插入、更改和移动之前删除。” "insertedIndexes" - "indexes are relative to the original fetch result (the fetchResultBeforeChanges property) after you’ve applied the changes described by the removedIndexes property; when updating your app’s interface, apply insertions after removals and before changes and moves."

"changedIndexes" 无法用 "delete"/"insert" 处理,因为它们描述了 insertions/deletions

之后的项目

"changedIndexes" - "这些索引是相对于应用更改后的原始提取结果(fetchResultBeforeChanges 属性)的由 removedIndexes 和 insertedIndexes 属性描述;更新应用程序界面时,在删除和插入之后以及移动之前应用更改。

警告 不要在批量更新中将 changedIndexes 直接映射到 UICollectionView 项目索引。在 performBatchUpdates(_:completion:) 之后使用这些索引重新配置相应的单元格。 UICollectionView 和 UITableView 期望 changedIndexes 处于 before 状态,而 PhotoKit 提供它们处于 after 状态,如果您的应用程序在更改的同时执行插入和删除,则会导致崩溃."

"enumerateMoves" 是我们应该做的最后一件事。

"enumerateMoves" - "处理程序块中的 toIndex 参数是相对于您应用由removedIndexes、insertedIndexes 和 changedIndexes 属性。因此,如果您使用此方法更新集合视图或显示获取结果内容的类似用户界面,请在处理之前更新 UI 以反映插入、删除和更改移动。"