UICollectionViewFlowLayout subclass crashes accessing array beyond bounds while scrolling and reloading

☆樱花仙子☆ 提交于 2019-12-31 12:08:53

问题


I have a UICollectionView that uses a custom subclass of UICollectionViewFlowLayout to make the section headers stick to the top of the screen while scrolling just like UITableView's plain style. My code is based on this approach:

http://blog.radi.ws/post/32905838158/sticky-headers-for-uicollectionview-using

In addition, the collection view is set up to load more results when the user scrolls to the bottom. That is, when the user gets to the bottom of the collection view, a web request loads more data and then reloads the collection view, i.e. calling reloadData.

It seems to work well except I've been getting a few crash reports from beta testers via TestFlight running iOS 7 and 7.1 who say that the following happens occasionally when they scroll down to the bottom (i.e. triggering more results to load) quickly:

*** -[__NSArrayM objectAtIndex:]: index 28 beyond bounds [0 .. 16]
PRIMARY THREAD THREAD 0

__exceptionPreprocess
objc_exception_throw
-[__NSArrayM objectAtIndex:]
-[UICollectionViewFlowLayout(Internal) _frameForItemAtSection:andRow:usingData:]
-[UICollectionViewFlowLayout layoutAttributesForItemAtIndexPath:usingData:]
-[UICollectionViewFlowLayout layoutAttributesForItemAtIndexPath:]
-[MyCustomCollectionViewFlowLayout layoutAttributesForItemAtIndexPath:]
-[MyCustomCollectionViewFlowLayout layoutAttributesForSupplementaryViewOfKind:atIndexPath:]
-[MyCustomCollectionViewFlowLayout layoutAttributesForElementsInRect:]
-[UICollectionViewData validateLayoutInRect:]_block_invoke
-[UICollectionViewData validateLayoutInRect:]
-[UICollectionView layoutSubviews]
-[UIView(CALayerDelegate) layoutSublayersOfLayer:]
-[CALayer layoutSublayers]

It seems as though when my custom flow layout code calls [self.collectionView numberOfItemsInSection:someSection] to get the layout attributes of a section's last item, that call returns based on the newly-loaded data (e.g. in this case that a section now has 29 items) but the internals of the default flow layout are still using some kind of cached data (e.g. in this case that the section only has 17 items). Unfortunately, I can't reproduce the crash myself and even the beta testers who have experienced it can't reproduce it consistently.

Any ideas what's going on there?


回答1:


Edit 2, as per 2nd BenRB's comment.

When dataSource gets updated and you call reloadData, this latter really invalidates everything in the collection view. However the logic and the exact sequence of the initiated refresh process happens inside the default flow layout and is hidden from us.

In particular, the default flow layout has its own private _prepareLayout (exactly, with underscore) method, which is independent from the prepareLayout and its overloads provided by the subclasses.

prepareLayout's (without underscore) default implementation of the base flow layout class does nothing, by the way.

During the refresh process the default flow layout gives its subclass a chance to provide more information (e.g. additional layoutAttributes) through layoutAttributesForElementsInRect: and layoutAttributesForItemAtIndexPath: "callbacks". To guarantee the consistency between base class' data and respective indexPath / layoutAttributes array, calls to corresponding "super" should only happen inside these respective methods:

  • [super layoutAttributesForElementsInRect:] only inside the overloaded [layoutAttributesForElementsInRect:]

  • [super layoutAttributesForItemAtIndexPath:] only inside the overloaded [layoutAttributesForItemAtIndexPath:],

No cross-calls between these methods should happen, at least with indexPaths not supplied by their corresponding "super" methods, because we don't know exactly what happens inside.

I was fighting with my collection view for a long time and ended with the only working sequence finally:

  1. Prepare add'l layout data by directly accessing the dataSource (without mediation of collection view's numberOfItemsInSection:), and store that data inside the subclass' object, e.g. in a dictionary property, using indexPath as a key. I am doing this in the overloaded [prepareLayout].

  2. Supply the stored layout data to the base class when it requests this information through callbacks:

// layoutAttributesForElementsInRect

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

//calls super to get the array of layoutAttributes initialised by the base class
**NSArray *array = [super layoutAttributesForElementsInRect:rect];**

for(MyLayoutAttributes *la in array)

    if(la.representedElementCategory == UICollectionElementCategoryCell ){
       NSIndexPath indexPath =  la.indexPath //only this indexPath can be used during the call to layoutAttributesForItemAtIndexPath:!!! 
       //extracts custom layout data from a layouts dictionary
       MyLayoutAttributes *cellLayout = layouts[la.indexPath];  
       //sets additional properties
        la.this = cellLayout.this
        la.that = cellLayout.that
        ...
    }
   ....
return array;
}

//layoutAttributesForItemAtIndexPath:

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {

  MyLayoutAttributes *la = (MyLayoutAttributes *)[super layoutAttributesForItemAtIndexPath:indexPath ];

    if(la.representedElementCategory == UICollectionElementCategoryCell ){
        NSIndexPath indexPath =  la.indexPath //only this indexPath can be used during the call !!! 
       //extracts custom layout data from a layouts dictionary using indexPath as a key
       MyLayoutAttributes *cellLayout = layouts[la.indexPath];  
       //sets additional properties
        la.this = cellLayout.this
        la.that = cellLayout.that
    }
return la;
}



回答2:


I'm now getting a crash that looks similar, i.e. where the layout is out of sync with the data source. This one I can reproduce and I believe I've found the cause. Since it is similar, I'm wondering if it is the same cause as my original crash/question. The error is:

UICollectionView recieved [sic] layout attributes for a cell with an index path that does not exist

My UICollectionViewFlowLayout subclass implements shouldInvalidateLayoutForBoundsChange: (so that I can position section headers at the top of the viewable bounds as the user scrolls). If a bounds change occurs and that method returns YES, the system requests a light-weight layout invalidation, i.e. all the properties on the invalidation context that is passed to the layout are NO: invalidateEverything, invalidateDataSourceCounts, invalidateFlowLayoutAttributes and invalidateFlowLayoutDelegateMetrics. If reloadData is then called afterwards (presumably before the layout has been reprocessed) then the system will not automatically call invalidateLayout, i.e. it will not trigger a heavy-weight invalidation context where all those properties are YES. So when that happens the layout apparently does not update its internal cache of section/item counts and such, which the leads to the layout being out of sync with the data source and a crash.

So for the particular situation I just ran into, I was able to work around it by simply calling invalidateLayout myself after calling reloadData. I'm not sure if this is the same situation as my original question/crash but it sure sounds like it.




回答3:


I have same problem. and I fixed it.

__weak typeof(self) weakSelf = self;
UICollectionView *collectionView = self.collectionView;
[UIView performWithoutAnimation:^{
    [collectionView performBatchUpdates:^{
        collectionView.collectionViewLayout = layout;
    } completion:^(BOOL finished) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf){
            [strongSelf reloadThumbData];
        }
    }];
}];



回答4:


I had the same issue although in my case with a custom layout. My solution was to call:

  • insertItems
  • deleteItems
  • updateItems

on the collectionView as appropriate to ensure that dataSource and layout are aware of the same number of items.

My layout is based on the data returned from:

super.layoutAttributesForElements(in: rect)

which was not returning the expected number of items that the dataSource indicated.

The documentation for insertItems seems to corroborate this:

Call this method to insert one or more new items into the collection 
view. You might do this when your data source object receives data for 
new items or in response to user interactions with the collection view. 
The collection view gets the layout information for the new cells as 
part of calling this method.


来源:https://stackoverflow.com/questions/24616797/uicollectionviewflowlayout-subclass-crashes-accessing-array-beyond-bounds-while

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