Prefetching in UICollectionView requests non-existent cells as part of performing batch updates when prefetching is enabled

Originator:horacek.eric
Number:rdar://30632322 Date Originated:2/21/2017
Status:Open Resolved:No
Product:UIKit Product Version:
Classification:Crash/Hang/Data Loss Reproducible:Sometimes
 
Description:
When `-[UICollectionView prefetchingEnabled]` is `YES`, there are conditions in which `UICollectionView` can request a cell that does not exist when the `-[UICollectionView performBatchUpdates:completion:]` method is invoked.

This has been observed as a crash in the production with our application. It has the following stack trace:

```
7   UIKit                                0x0000000198447c0c -[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:isFocused:notify:] + 444
8   UIKit                                0x0000000198448edc -[UICollectionView _prefetchItemsForVelocity:maxItemsToPrefetch:invalidateCandidatesOnDirectionChanges:] + 496
9   UIKit                                0x0000000197bba73c -[UICollectionView layoutSubviews] + 688
10  UIKit                                0x0000000197b5ba80 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1192
11  QuartzCore                           0x00000001950099d8 -[CALayer layoutSublayers] + 144
12  QuartzCore                           0x0000000194ffe4cc CA::Layer::layout_if_needed(CA::Transaction*) + 288
13  UIKit                                0x0000000197b70500 -[UIView(Hierarchy) layoutBelowIfNeeded] + 1012
14  UIKit                                0x000000019845beec -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:animator:] + 240
15  UIKit                                0x000000019845bdd8 -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:] + 92
16  UIKit                                0x000000019845bd5c -[UICollectionView _performBatchUpdates:completion:invalidationContext:] + 80
17  UIKit                                0x0000000197d38618 -[UICollectionView performBatchUpdates:completion:] + 60
```

From this stack trace, you can observe that the UICollectionView is is requesting a new cell from its `dataSource` as a side-effect of the `performBatchUpdates:completion:` method. This would be normally be expected behavior, except for the fact that the cell that's being requested is one that doesn't exist.

When the `[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:animator:]` method is disassembled, you can see that the `-[UIView layoutBelowIfNeeded]` method referenced from the above stack trace is invoked before the batch updates are made.

```
void -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:animator:](void * self, void * _cmd, void * arg_8, void * arg_C, void * arg_10, char arg_14, void * arg_18) {
    ...
    if ([self _visible] != 0x0) {
            ...
            [esi layoutBelowIfNeeded];
            [esi _beginUpdates];
            ...
    }
    else {
        ...
    }
    ...
    return;
}
``` 

Specifically, note that `[esi layoutBelowIfNeeded]` is invoked before `[esi _beginUpdates]`, which appears to apply the updates provided in the `batchUpdates` block to the collection view's internal representation of the items and sections. This would imply that the data inconsistency is resulting from a logic error whereby `UICollectionView` does not prevent itself from requesting old cells prior to the application of batch updates.

This can be tied to prefetching by the fact that `_prefetchItemsForVelocity:maxItemsToPrefetch:invalidateCandidatesOnDirectionChanges:` is included in the stack trace, which is only invoked if prefetching is enabled.

Additionally, we did not observe this crash before prefetching was enabled.


Steps to Reproduce:
- Set enablePrefetching to YES on a UICollectionView
- Perform batch updates on the content of the UICollectionView


Expected Results:
UICollectionView does not request non-existent cells. 


Actual Results:
Cells that no longer exist are requested by the UICollectionView, which causes a crash when nil is returned from `-[UICollectionViewDataSource collectionView:cellForItemAtIndexPath:]`


Configuration:
Seen on a variety of devices

Version & Build:
iOS 10.2.1 (14D27)

Comments


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!