UICollectionView性能- _updateVisibleCellsNow

53

我正在开发一个自定义的UICollectionViewLayout,它按日/周/月组织单元格。

它无法平滑滚动,并且看起来延迟是由于每次渲染循环都调用[UICollectionView _updateVisibleCellsNow]引起的。

对于小于30个项目,性能还可以,但在大约100个或更多项目时,它变得非常慢。这是UICollectionView和自定义布局的限制,还是我没有为视图提供足够的信息以正确执行?

此处源代码:https://github.com/oskarhagberg/calendarcollection

布局:https://github.com/oskarhagberg/calendarcollection/blob/master/CalendarHeatMap/OHCalendarWeekLayout.m

数据源和委托:https://github.com/oskarhagberg/calendarcollection/blob/master/CalendarHeatMap/OHCalendarView.m

时间分析图: Time profile - Custom layout

更新

也许这是徒劳的?使用一个具有大约相同数量的单元格/屏幕的UICollectionViewFlowLayout的简单UICollectionViewController进行一些测试,结果产生了类似的时间分析。

Time profile - Standard flow layout

我觉得它应该能够处理大约100个简单不透明单元格而没有抖动。我错了吗?


不,你没有错。我的测试使用基本的UICollectionView对象,使用Flowlayout和包含图像视图对象的简单静态分配单元格数组,显示您可以轻松达到100个单元格而不会出现性能问题。在iPad 3上进行基本设置时,当达到700多个单元格时,预计会出现性能问题。 - TheBasicMind
1
在我的情况下,与NSDate相关的函数需要太多时间... - gdm
你能解释一下你是如何创建你的基础单元格集合视图的吗?在iPad 3上,单元格只包含1个图像(所有单元格都相同),即使有50个单元格,它也不完全流畅,并且超过150个单元格就完全无法使用。此外,当我说150个单元格时,我的意思是一次在屏幕上可见的数量,而不是整个数据集中的数量。在屏幕上有30个单元格时,即使有巨大的数据集(100K+),我也可以轻松保持平稳。 - Fabien Warniez
10个回答

93

还要记得尝试栅格化单元格的图层:

cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;

没有这个功能时我的帧率是10,有了之后瞬间变成55! 由于我对GPU和合成模型不是很熟悉,所以它确切地做了什么对我来说不是很清楚,但基本上它会将所有子视图的渲染压缩成一个位图(而不是每个子视图一个位图)。 无论如何,我不知道它是否有一些缺点,但速度提升非常快!


我在使用iOS7时需要添加这个,但在iOS6中不需要,但我的FPS很不流畅(50),现在是59,非常顺畅! - DogCoffee
此外,在上述代码后,我需要在 cellForItemAtIndexPath 中调用 [cell layoutSubviews]。 - Itachi
缺点之一是所有内容都驻留在内存中。这应该进行基准测试,并且对于不同的用例可能会有所不同。如此提到的:https://dev59.com/SWgu5IYBdhLWcg3wLEI9 - user5890979

18
我一直在对UICollectionView的性能进行大量研究。结论很简单,对于大量单元格的性能较差。

编辑:抱歉,我重新阅读了您的帖子,您拥有的单元格数量应该没问题(请参见我的其他评论),因此单元格复杂性也可能是一个问题。

如果您的设计支持,请检查:

  1. 每个单元格都是不透明的。

  2. 单元格内容剪切到边界。

  3. 单元格坐标位置不包含小数值(例如始终计算为整个像素)

  4. 尽量避免重叠单元格。

  5. 尽量避免投影阴影。



原因其实很简单。很多人不理解这一点,但值得理解的是:UIScrollView没有使用核心动画来滚动。我天真地认为它们涉及某些秘密滚动动画“酱汁”,并且偶尔从代理那里请求更新以检查状态。但事实上,滚动视图只是一个类,它应用了一个数学函数来抽象UIView的坐标放置,因此视图的坐标被视为相对于一个抽象的contentView平面而不是相对于包含视图的原点。滚动视图将根据用户输入(例如滑动)更新抽象滚动平面的位置,并且当然还有物理算法,可以给转换后的坐标位置带来“动量”。

现在,如果您要制作自己的集合视图布局对象,在理论上,您可以产生一个完全颠倒底层滚动视图应用的数学翻译的集合视图布局对象。这很有趣但没用,因为它看起来似乎单元格根本没有移动,而您只是在滑动。但我提出这种可能性是因为它说明了集合视图布局对象与集合视图对象本身一起工作时执行了与底层滚动视图非常相似的操作。例如,它只是提供了一个机会,通过逐帧翻译要显示的视图的属性来应用额外的数学框架,通常这只是位置属性的翻译。

只有在插入、删除、移动或重新加载新单元格时才涉及核心动画;最常见的方法是调用:

- (void)performBatchUpdates:(void (^)(void))updates
                 completion:(void (^)(BOOL finished))completion

UICollectionView为滚动的每个帧和每个可见视图请求单元格布局属性,并且每个帧为每个视图布局。UIView是优化灵活性而不是性能的丰富对象。每次布局时,系统会执行一些测试来检查其alpha、zIndex、子视图、剪贴等属性。清单很长。这些检查以及对视图所做的任何更改都将为每个集合视图项的每个框架进行。
为确保良好的性能,所有逐帧操作都需要在17ms内完成。[由于您拥有的单元格数量,这根本不可能发生]用括号括起来,因为我重新阅读了您的帖子,意识到我误读了它。由于您拥有的单元格数量,不应该存在性能问题。我个人发现,在iPad 3上进行简化测试,只包含单个缓存图像的原始单元格,在屏幕上显示约784个单元格后,性能开始下降至50fps以下。
实际上,您需要让它少于这个数字。
就我个人而言,我正在使用自己的自定义布局对象,需要比UICollectionView提供的更高的性能。不幸的是,直到开发路径的某个地方,我才运行了上述简单测试,并意识到存在性能问题。我要重新制作UICollectionView的开源后移版本PSTCollectionView。我认为可以实现一个解决方法,以便每个单元格项的图层在一般滚动时都是使用操作编写的,从而绕过了每个UIView单元格的布局。这对我来说是有效的,因为我有自己的布局对象,我知道何时需要布局,并且我有一个巧妙的技巧,可以在此时让PSTCollectionView回退到其正常的操作模式。我已经检查了调用顺序,它看起来并不太复杂,也不完全不可行。但是肯定是非平凡的,必须进行进一步的测试,然后我才能确认它是否有效。

我的性能问题出在iPhone5上。也许iPad 3可以在屏幕上挤入更多的单元格。你提到UICollectionView为每个帧请求layoutAttributes,但在我的情况下,我发现并非如此。当滚动时,它只会请求一些大小为屏幕尺寸的矩形区域内的属性数组。所以我想它应该能够在滚动视图的内容视图上布置单元格,然后简单地调整偏移量(就像你所说的那样)。但即使我只是微调视图上下几个像素(没有请求属性),它仍然很卡顿。 - Oskar
很有趣,你提到了PSTCollectionView,因为这也是我害怕不得不退而求其次的东西。虽然感觉像是一个巨大的工程。要么就是将一些UIViews分组到较少的UICollectionViewCell中。仍然是相同数量的UIView/屏幕,但是少了许多单元格。但是使用这种解决方案,我会失去在布局之间变形和进行批量更新的能力,这会让人失望。或者只是忍受滞后,并希望他们在iOS7中改进它。 - Oskar
回复:关于 PST Collection view,提高滚动性能确实是一项非常不容易的任务,如果您必须构建自己的布局对象来完成它,则需要进行更大的工作。然而,我已经完成了这个任务(因为我需要类似电子表格的x,y轴滚动),所以有了一个布局对象,虽然它很难,但同样需要进行相当核心的手术修改。我们拭目以待! - TheBasicMind
从siteCell层中删除阴影颜色、半径、偏移和不透明度似乎改善了我的滚动性能。 - topwik

8
一些可能有用的观察:
使用大约175个可见项的流式布局,我能够重现这个问题:在模拟器中可以平滑滚动,但在iPhone 5上却非常卡顿。确保它们是不透明的等等。
最耗时的工作似乎是在_updateVisibleCellsNow内部使用可变NSDictionary。无论是复制字典还是按键查找项目都很耗时。键似乎是UICollectionViewItemKey,[UICollectionViewItemKey isEqual:]方法是所有方法中最耗时的。UICollectionViewItemKey至少包含type、identifier和indexPath属性,其中包含的indexPath比较[NSIndexPath isEqual:]需要最长时间。
从这里推测,UICollectionViewItemKey的哈希函数可能不足,因为在字典查找期间经常调用isEqual:。许多项目可能会以相同的哈希值(或在同一个哈希桶中,不确定NSDictionary如何工作)结束。
由于某种原因,在一个部分中放置所有项比在每个部分中放置7个项要快。可能是因为它花费了太多时间在NSIndexPath isEqual上,如果行差异首先出现,或者UICollectionViewItemKey获得更好的哈希,则速度会更快。
老实说,感觉UICollectionView在每次滚动帧都要做那么重的字典工作真的很奇怪,正如之前提到的,每个帧更新需要小于16ms才能避免卡顿。我想知道这么多的字典查找是否是:
一般UICollectionView操作所必需的
用于支持某些很少使用的边缘情况,可以关闭大多数布局
一些未经优化的内部代码尚未修复
我们的某些错误
希望我们今年夏天在WWDC上能看到一些改进,或者如果这里有人能够找出如何解决它。

很好的观察。也许这是需要提交一个雷达反馈的事情。 - Oskar
当你考虑到这一点时,字典查找是必需的,因为与表视图不同,单元格的显示没有保证的顺序。您可以定义单元格在任何时间遵循任何规则,并且集合视图需要知道对于任何给定的帧,哪些单元格在屏幕上以及它们应该放置在哪里(即使单元格从第1、2和3节可见,仍然不能保证所有来自第2节的单元格都在屏幕上)。当您自己设计布局时,这变得非常清晰。数组行不通,因为所需的索引路径经常是不连续的。 - TheBasicMind
Andreas,你是否正在自己创建Layout对象?我也发现NSDictionary的性能问题,但是当我开始缓存任何生成的layoutAttributes对象时,这个问题好像有所缓解。如果不缓存这些对象,则会出现糟糕的情况,因为对于给定的IndexPath,将返回完全相同的属性对象,当未来请求布局对象时。我说“好像”是因为不幸的是,直到一段时间后我才注意到这个指标的改善,并且还没有运行特定的测试来确认。 - TheBasicMind
有趣的想法,但我认为这会倾向于未优化的内部代码。集合视图一次性向布局请求完整屏幕的 layoutAttributes,然后在内部保存它们。在滚动时没有数据源或代理调用可能会导致延迟,只有内部工作。 - Andreas Karlsson
CollectionView每次请求一整个屏幕的内容,但如果您滚动回已经“看过”的区域,则不会再次请求。而且它不会在每次滚动时都请求,只有在新的一屏内容即将变为可见时才会请求一次。然后,它将所有布局属性保留在内部的“已看过”区域空间中。似乎是这个内部数据存储和查找出了问题/效率低下。如果您希望它每帧都要求布局属性,我想您必须不断地使布局无效,这可能会更加卡顿。 - Andreas Karlsson
显示剩余2条评论

7

这里是Altimac的答案转换为Swift 3:

cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale

此外,需要注意的是这段代码应该放在collectionView代理方法cellForItemAtIndexPath中。
还有一个提示-要查看应用程序的每秒帧数(FPS),请在Instruments下打开核心动画(参见截图)。

enter image description here


1
应该使用UIScreen.mainScreen().scale而不是UIScreen.main.scale。 - Mat0
3
Mat0,你使用的是哪个版本的Swift?在Swift 3中,UIScreen.main.scale是首选方法...UIScreen.mainScreen().scale现在已经是“老派”的pre-Swift 3了!;-) - Gene Loparco

6
问题不在于你在集合视图中显示的单元格总数,而是一次屏幕上显示的单元格数量。由于单元格大小非常小(22x22),因此一次可以看到154个单元格。渲染每个单元格会减慢您的界面速度。您可以通过增加Storyboard中的单元格大小并重新运行应用程序来证明这一点。
不幸的是,你无法做太多事情。我建议通过避免剪切边界和尽量不实现drawRect:来缓解问题,因为这样会很慢。

1
这里指的是单元格/屏幕。我应该澄清一下:)。我还有一些没有子级(和没有drawRect的)简单单元格。从概要上看,集合视图在每个渲染循环中执行所有(可见)属性的副本。这就是CPU时间消耗的地方,而不是绘图。 - Oskar
1
drawRect提示帮助我解决了我的问题。 - Fostah

6

以上两个答案都很好!这里还有一个额外的建议:我通过禁用自动布局,改善了UICollectionView的性能。虽然您需要添加一些额外的代码来布局单元格内部,但定制代码似乎比自动布局快得多。


不幸的是,在我的情况下禁用自动布局并没有帮助。在开启和关闭时表现相同。我已在Storyboard层面上将其关闭。您是否知道是否可能仅在单元格中关闭它?它们很简单,不需要任何花哨的布局。 - Oskar
我从未尝试过,但这可能会有所帮助:https://dev59.com/CGgu5IYBdhLWcg3wYGG3#15242064 - NSSplendid

5
除了列出的答案(如光栅化、自动布局等),您可能还想检查其他可能拖慢性能的原因。
在我的情况下,每个UICollectionViewCell都包含另一个UICollectionView(每个UICollectionView中有大约25个单元格)。当加载每个单元格时,我调用内部UICollectionView.reloadData(),这显著拖慢了性能。
然后我把reloadData放在主UI队列中,问题就解决了:
DispatchQueue.main.async {
    self.innerCollectionView.reloadData()
}

仔细研究这些原因可能也有所帮助。

4

在一些情况下,这是由于UICollectionViewCell中的自动布局引起的。关闭它(如果您可以不使用它),滚动将变得非常流畅 :) 这是一个iOS问题,他们很久以前就没有解决了。


1
谢谢你!你救了我的一天!!!在自定义键盘扩展中,禁用单元格Xib文件中的自动布局,使iPhone 4S iOS 8.1.4上的帧速率从5-10提高到了60fps。 - imike
这对我的卡顿iPhone 4S有很大帮助。 对于那些这样做的人,您可以通过将控制器放在其自己的故事板中来仅为UICollectionView禁用自动布局。 要轻松将其放入其自己的Storyboard中,请在Interface Builder中使用“ Editor-> Refactor to Storyboard ...” - Vegard

3
如果您正在实现网格布局,您可以通过为每一行使用单个UICollectionViewCell并向其中添加嵌套的UIView来解决此问题。我实际上是子类化了UICollectionViewCellUICollectionReusableView,并覆盖了prepareForReuse方法以删除所有的子视图。在collectionView:cellForItemAtIndexPath:中,我添加了所有原始单元格作为subviews,并将它们的框架设置为原始实现中使用的x坐标,并将其y坐标调整为在cell内部。使用这种方法,我能够仍然使用UICollectionView的某些好处,例如targetContentOffsetForProposedContentOffset:withScrollingVelocity:,以在单元格的顶部和左侧对齐。我从获得 4-6 FPS 到平滑的 60 FPS

0

我想快速分享一下我的解决方案,因为我遇到了一个非常类似的问题 - 基于图像的UICollectionView。

在我所在的项目中,我通过网络获取图像,在设备上进行本地缓存,然后在滚动期间重新加载缓存的图像。

我的错误在于我没有在后台线程中加载缓存的图像。

一旦我将[UIImage imageWithContentsOfFile:imageLocation];放入后台线程中(然后通过主线程将其应用于我的imageView),我的FPS和滚动效果就好多了。

如果您还没有尝试过,请务必尝试一下。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接