UICollectionViewFlowLayout 子类在滚动和重新加载时崩溃访问超出范围的数组
Posted
技术标签:
【中文标题】UICollectionViewFlowLayout 子类在滚动和重新加载时崩溃访问超出范围的数组【英文标题】:UICollectionViewFlowLayout subclass crashes accessing array beyond bounds while scrolling and reloading 【发布时间】:2014-07-07 17:57:42 【问题描述】:我有一个 UICollectionView,它使用 UICollectionViewFlowLayout 的自定义子类来使部分标题在滚动时粘在屏幕顶部,就像 UITableView 的普通样式一样。我的代码是基于这种方法的:
http://blog.radi.ws/post/32905838158/sticky-headers-for-uicollectionview-using
此外,集合视图设置为在用户滚动到底部时加载更多结果。也就是说,当用户到达集合视图的底部时,一个网络请求会加载更多的数据,然后重新加载集合视图,即调用 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]
似乎当我的自定义流布局代码调用 [self.collectionView numberOfItemsInSection:someSection]
以获取节最后一项的布局属性时,该调用根据新加载的数据返回(例如,在这种情况下,一个节现在有 29 个项目) 但默认流布局的内部仍在使用某种缓存数据(例如,在这种情况下,该部分只有 17 个项目)。不幸的是,我自己无法重现崩溃,即使是经历过它的 beta 测试人员也无法始终如一地重现它。
有什么想法吗?
【问题讨论】:
【参考方案1】:编辑 2,根据 2nd BenRB 的评论。
当 dataSource 得到更新并调用 reloadData 时,后者实际上会使集合视图中的所有内容无效。 然而,初始化刷新过程的逻辑和确切顺序发生在默认流布局内,对我们隐藏。
特别是,默认流布局有自己的私有_prepareLayout
(确切地说,是带下划线)方法,它独立于prepareLayout
及其子类提供的重载。
prepareLayout
的(不带下划线)基本流布局类的默认实现什么也不做。
在刷新过程中,默认流布局使其子类有机会通过layoutAttributesForElementsInRect:
和layoutAttributesForItemAtIndexPath:
“回调”提供更多信息(例如额外的 layoutAttributes)。为了保证基类的数据和相应的 indexPath / layoutAttributes 数组的一致性,对相应“super”的调用应该只发生在这些相应的方法内部:
[super layoutAttributesForElementsInRect:]
仅在
重载[layoutAttributesForElementsInRect:]
[super layoutAttributesForItemAtIndexPath:]
仅在重载的[layoutAttributesForItemAtIndexPath:]
内,
这些方法之间不应发生交叉调用,至少对于它们相应的“超级”方法未提供的 indexPaths 而言,因为我们不确切知道内部发生了什么。
我与收藏视图斗争了很长时间,最终以唯一的工作序列结束:
通过直接访问 dataSource 准备添加布局数据(无需中介集合视图的 numberOfItemsInSection:),并将该数据存储在子类的对象中,例如在字典属性中,使用 indexPath 作为键。我正在重载的[prepareLayout]
中执行此操作。
当基类通过回调请求此信息时,将存储的布局数据提供给基类:
// 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;
【讨论】:
谢谢,弗拉基米尔。我实际上没有实现prepareLayout。我主要是实现 layoutAttributesForElementsInRect:。为了保持一致性,我还实现了 layoutAttributesForSupplementaryViewOfKind:atIndexPath: 和 layoutAttributesForItemAtIndexPath:。我的简历数据源只是一个数组。顺序本质上是:用户滚动到底部,web请求返回数组,数组被添加到现有数组中,我调用reloadData,框架调用layoutAttributesForElementsInRect:,我调用numberOfItemsInSection:获取新数据和[super layoutAttributesForItemAtIndexPath:]由于旧数据而崩溃. 好的。因此,您认为默认流布局是联系 CV 数据源以获取 prepareLayout 中的节数,然后我更改数组,调用 reloadData,然后框架调用 layoutAttributesForElementsInRect:? IE。 prepareLayout 不会在 layoutAttributesForElementsInRect 之前发生:?因此,为了与默认流布局的内部保持一致,对 CV 数据源的调用必须发生在 prepareLayout 中,然后在 layoutAttributesForElementsInRect 中使用:? UICollectionViewLayoutInvalidationContext 文档听起来像是 reloadData 使一切无效。 reloadData 的调用确实使 CV 中的所有内容都无效。然而,启动刷新过程的逻辑发生在默认流布局内,对我们隐藏。我们不知道默认流布局在什么时候构建其内部数组。因此,除非我们 100% 确定它们处于实际状态,否则我们不应使用这些内部结构。通过调用layoutAttributesForItemAtIndexPath:
并使用不是从默认流布局而是从外部源接收的 indexPath(s),您可能会请求不一致或不存在的数据。请查看我的更新答案。
我重写了我的代码来解决这个问题。现有:从数据源获取部分信息以创建部分最后一项的索引路径,将其传递给 layoutAttributesForItemAtIndexPath:以获取默认位置以计算最下方的标题可以在被下一部分推离屏幕顶部之前。新增内容:使用默认 layoutAttributesForElementsInRect 的返回值:在部分中获取最下方的屏幕项目。并不总是给出真正的最后一个元素,因为它可能在屏幕外,但在这种情况下,标题不需要向下那么远,因为它会被卡在屏幕的顶部或默认位置。
我不喜欢这个解决方法的一点是我无法实现 layoutAttributesForSupplementaryViewOfKind:atIndexPath: 来返回准确的值,因为它需要访问数据源并获取项目的默认布局属性.但是在我的使用中,似乎没有调用该方法。文档说要实现它。而且我仍然觉得必须有一种方法可以查询数据源并知道默认流布局没有过期缓存。现在我会接受,继续这样做,如果我想出更好的东西或者这不起作用,请报告。【参考方案2】:
我现在遇到了一个看起来相似的崩溃,即布局与数据源不同步。这个我可以重现,我相信我已经找到了原因。由于它相似,我想知道它是否与我原来的崩溃/问题相同。错误是:
UICollectionView 收到了 [sic] 单元格的布局属性,其索引路径不存在
我的UICollectionViewFlowLayout
子类实现了shouldInvalidateLayoutForBoundsChange:
(这样我可以在用户滚动时将部分标题定位在可视边界的顶部)。如果发生边界更改并且该方法返回YES
,则系统请求轻量级布局失效,即传递给布局的失效上下文上的所有属性都是NO
:invalidateEverything
,invalidateDataSourceCounts
, invalidateFlowLayoutAttributes
和 invalidateFlowLayoutDelegateMetrics
。如果之后调用reloadData
(大概在重新处理布局之前),那么系统将不会自动调用invalidateLayout
,即它不会触发所有这些属性都是YES
的重量级失效上下文。因此,当这种情况发生时,布局显然不会更新其内部缓存的部分/项目计数等,这会导致布局与数据源不同步并崩溃。
因此,对于我刚刚遇到的特殊情况,我只需在调用reloadData
后自己调用invalidateLayout
就可以解决它。我不确定这是否与我最初的问题/崩溃情况相同,但听起来确实如此。
【讨论】:
我修好了。 'shouldInvalidateLayoutForBoundsChange' 在 'reloadData' 之前返回 NO【参考方案3】:我也有同样的问题。我修好了。
__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】:我遇到了同样的问题,尽管在我的情况下是自定义布局。 我的解决方案是致电:
插入项目 删除项目 更新项目在 collectionView 上酌情确保 dataSource 和 layout 知道相同数量的项目。
我的布局基于从以下位置返回的数据:
super.layoutAttributesForElements(in: rect)
它没有返回 dataSource 指示的预期项目数。
insertItems 的文档似乎证实了这一点:
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.
【讨论】:
以上是关于UICollectionViewFlowLayout 子类在滚动和重新加载时崩溃访问超出范围的数组的主要内容,如果未能解决你的问题,请参考以下文章