在滚动和重新加载时,UICollectionViewFlowLayout子类崩溃访问超出边界的数组

Ben*_*nRB 7 ios uicollectionview uicollectionviewlayout

我有一个UICollectionView使用UICollectionViewFlowLayout的自定义子类,使部分标题贴在屏幕的顶部,同时滚动就像UITableView的简单样式.我的代码基于这种方法:

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

此外,集合视图设置为在用户滚动到底部时加载更多结果.也就是说,当用户到达集合视图的底部时,Web请求会加载更多数据,然后重新加载集合视图,即调用reloadData.

它似乎运行良好,除了我通过运行iOS 7和7.1的TestFlight从beta测试者那里得到一些崩溃报告,他们说当他们快速向下滚动到底部(即触发更多结果加载)时偶尔发生以下情况:

*** -[__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]
Run Code Online (Sandbox Code Playgroud)

好像当我的自定义流布局代码调用[self.collectionView numberOfItemsInSection:someSection]以获取节的最后一个项的布局属性时,该调用基于新加载的数据返回(例如,在这种情况下,一个节现在有29个项)但是内部的默认流布局仍然使用某种缓存数据(例如,在这种情况下,该部分只有17个项目).不幸的是,我无法自己重现崩溃,即使是经历过它的beta测试人员也无法一致地重现它.

有什么想法吗?

小智 7

根据2 BenRB的注释,编辑2

当dataSource得到更新并调用reloadData时,后者实际上会使集合视图中的所有内容失效。但是,初始化刷新过程的逻辑和确切顺序发生在默认流布局内部,对我们而言是隐藏的。

特别是,默认流布局具有其自己的私有方法_prepareLayout(恰好带有下划线),该方法独立于prepareLayout子类提供的方法及其重载。

prepareLayout顺便说一下,基本流布局类的默认实现(没有下划线)不执行任何操作。

在刷新过程中,默认流程布局为其子类提供了通过layoutAttributesForElementsInRect:layoutAttributesForItemAtIndexPath:“回调” 提供更多信息(例如,额外的layoutAttributes)的机会 。为了保证基类的数据和相应的indexPath / layoutAttributes数组之间的一致性,对相应“ super”的调用应仅在以下相应方法内进行:

  • [super layoutAttributesForElementsInRect:] 仅在超载的内部 [layoutAttributesForElementsInRect:]

  • [super layoutAttributesForItemAtIndexPath:]仅在超载的内部[layoutAttributesForItemAtIndexPath:]

这些方法之间不应发生任何交叉调用,至少对于没有由其相应的“ super”方法提供的indexPath而言,因为我们不确切知道内部发生了什么

我与集合视图进行了很长时间的斗争,最后以唯一的工作序列结束:

  1. 通过直接访问dataSource(无需中介视图的numberOfItemsInSection:的中介)来准备附加布局数据,并将该数据存储在子类的对象内部,例如,使用indexPath作为键存储在字典属性中。我正在超负荷运行[prepareLayout]

  2. 当它通过回调请求此信息时,将存储的布局数据提供给基类​​:

// 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;
}
Run Code Online (Sandbox Code Playgroud)

// 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;
}
Run Code Online (Sandbox Code Playgroud)