UICollectionView 性能 - _updateVisibleCellsNow
Posted
技术标签:
【中文标题】UICollectionView 性能 - _updateVisibleCellsNow【英文标题】:UICollectionView performance - _updateVisibleCellsNow 【发布时间】:2013-05-02 11:21:29 【问题描述】:我正在开发一个自定义 UICollectionViewLayout
,它显示按天/周/月组织的单元格。
滚动不流畅,看起来延迟是由于在每个渲染循环上调用 [UICollectionView _updateVisibleCellsNow]
造成的。
来源: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
时间简介:
更新
也许它是徒劳的?一些使用普通UICollectionViewController
和UICollectionViewFlowLayout
的测试,给定大致相同数量的单元格/屏幕会产生相似的时间曲线。
我觉得它应该能够一次处理约 100 个简单的不透明单元而不会出现抖动。我错了吗?
【问题讨论】:
不,你没有错。我使用 Flowlayout 和包含图像视图对象的静态分配单元格的简单数组对 vanilla UICollectionView 对象进行的测试表明,您可以轻松达到 100 个单元格而不会出现性能问题。使用 iPad 3 上的普通设置,大约 700 多个单元预计会出现性能问题。 在我的例子中,NSDate 相关的函数花费了太多时间...... 你能解释一下你是如何创建你的原版细胞集合视图的吗?在 iPad 3 上,单元格仅包含 1 个图像(所有单元格都相同),即使有 50 个单元格,它也不是完全平滑,超过 150 个单元格完全无法使用。另外,当我说 150 个单元格时,我的意思是在屏幕上立即可见,而不是在整个数据集中。屏幕上有 30 个单元格,即使有庞大的数据集 (100K+),我也可以轻松保持流畅。 【参考方案1】:也不要忘记尝试栅格化单元格的图层:
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
没有那个我有 10 fps,有 55 fps ! 我不太熟悉 GPU 和合成模型,所以它到底做了什么对我来说并不完全清楚,但基本上它将所有子视图的渲染展平在一个位图中(而不是每个子视图一个位图?)。 无论如何我不知道它是否有一些缺点,但它的速度要快得多!
【讨论】:
我在使用 ios7 时需要添加此功能,在 ios6 中不需要,但我的 FPS 不稳定 (50),现在 59 并且流畅如丝! 另外,我需要在上面的代码之后调用cellForItemAtIndexPath中的[cell layoutSubviews]。 其中的缺点是一切都驻留在内存中。这应该进行基准测试,并且对于不同的用例可能会有所不同。如此处所述:***.com/questions/11521959/…【参考方案2】:我一直在对 UICollectionView 的性能进行大量调查。结论很简单。大量单元的性能很差。
编辑:抱歉,请重新阅读您的帖子,您拥有的单元格数量应该没问题(请参阅我的评论的其余部分),因此单元格复杂性也可能是一个问题。
如果您的设计支持,请检查:
每个单元格都是不透明的。
单元格内容剪辑到边界。
单元格坐标位置不包含小数值(例如,始终计算为整个像素)
尽量避免重叠单元格。
尽量避免阴影。
原因其实很简单。很多人不理解这一点,但值得理解的是:UIScrollViews 没有使用 Core Animation 来滚动。我天真地认为,他们涉及一些秘密的滚动动画“酱”,只是不时地要求代表们偶尔更新以检查状态。但实际上滚动视图是根本不直接应用任何绘图行为的对象。它们实际上只是一个应用数学函数抽象它们包含的 UIViews 坐标位置的类,因此 Views 坐标被视为相对于抽象 contentView 平面而不是相对于包含视图的原点。滚动视图将根据用户输入(例如滑动)更新抽象滚动平面的位置,当然还有一种物理算法可以为翻译的坐标位置提供“动量”。
现在,如果您要生成自己的集合视图布局对象,理论上,您可以生成一个 100% 反转底层滚动视图应用的数学转换的对象。这会很有趣但没用,因为当你滑动时,细胞似乎根本没有移动。但我提出了这种可能性,因为它说明了集合视图布局对象与集合视图对象本身的操作与底层滚动视图非常相似。例如。它只是提供了一个机会,可以对要显示的视图的属性进行逐帧的数学转换,主要是位置属性的转换。
只有在插入或删除新单元格、移动或重新加载时才会涉及 CoreAnimation;通常通过调用:
- (void)performBatchUpdates:(void (^)(void))updates
completion:(void (^)(BOOL finished))completion
UICollectionView 为每一帧滚动请求单元格 layoutAttributes 并且为每一帧布局每个可见视图。 UIView 是针对灵活性而非性能进行优化的丰富对象。每次布局时,系统都会进行许多测试来检查它的 alpha、zIndex、子视图、剪辑属性等。列表很长。这些检查以及由此产生的对视图的任何更改都是针对每个框架的每个集合视图项进行的。
为确保良好的性能,所有逐帧操作都需要在 17 毫秒内完成。 [由于您拥有的单元格数量,这根本不会发生]将这个条款括起来,因为我已经重新阅读了您的帖子,并且我意识到我误读了它。有了你拥有的细胞数量,应该不会有性能问题。我个人发现使用香草单元进行简化测试,仅包含一个缓存图像,在 iPad 3 上测试的限制是大约 784 个屏幕单元,然后性能开始下降到 50fps 以下。
在实践中,您需要保持比这个少。
我个人使用的是我自己的自定义布局对象,并且需要比 UICollectionView 提供的更高的性能。不幸的是,我没有运行上面的简单测试,直到开发路径的某个方向,我意识到存在性能问题。所以我将重新设计 UICollectionView 的开源后端端口 PSTCollectionView。我认为有一种解决方法可以实现,因此,仅对于一般滚动,每个单元格项目的层都是使用绕过每个 UIView 单元格布局的操作编写的。这对我有用,因为我有自己的布局对象,并且我知道何时需要布局,并且我有一个巧妙的技巧,可以让 PSTCollectionView 在此时回退到其正常操作模式。我检查了调用顺序,它似乎并没有太复杂,也没有出现完全不可行的情况。但可以肯定的是,这不是微不足道的,在我确认它会起作用之前,必须进行一些进一步的测试。
【讨论】:
我的性能问题出在 iPhone5 上。也许 iPad 3 可以塞进更多的单元格/屏幕。您提到UICollectionView
为每一帧请求layoutAttributes
,但就我而言,我发现并非如此。它在滚动时只请求几次具有屏幕大小的矩形的属性数组。所以我认为它应该能够在滚动视图内容视图上布置单元格,然后简单地调整偏移量(就像你说的那样)。但即使我只是将视图上下微调几个像素(没有请求属性),它仍然滞后。
你提到PSTCollectionView
很有趣,因为我担心我也不得不依赖它。不过感觉像是一项艰巨的任务。或者将一些UIViews
组合成更少的UICollectionViewCell
s。 UIView
/screen 的总量仍然相同,但单元格更少。但是使用该解决方案,我失去了在布局之间变形和进行批量更新的能力,这将是一个无赖。或者干脆忍受滞后,并希望他们在 iOS7 中改进它。
Re: PST Collection 视图,如果您必须构建自己的布局对象来提高滚动性能,这当然不是一件小事,而且是一项更大的工作。但是我已经这样做了(因为我需要像 x,y 轴滚动这样的电子表格),所以手头有一个布局对象,尽管它可能不是微不足道的,但它同样需要一个非常有核的手术修改。我们会看到的!
从 siteCell 层移除阴影颜色、半径、偏移量和不透明度似乎提高了我的滚动性能。字。【参考方案3】:
更多可能有用的观察结果:
我能够重现这个问题,使用流布局一次可以看到大约 175 个项目:它在模拟器中平滑滚动,但在 iPhone 5 上像地狱一样滞后。确保它们是不透明的等等。
最终花费最多时间的似乎是在_updateVisibleCellsNow
中使用可变的 NSDictionary。既复制字典,又按键查找项目。键似乎是 UICollectionViewItemKey 而[UICollectionViewItemKey isEqual:]
方法是最耗时的方法。 UICollectionViewItemKey至少包含type
、identifier
和indexPath
属性,其中包含的属性indexPath比较[NSIndexPath isEqual:]
耗时最多。
据此我猜测可能缺少 UICollectionViewItemKey 的哈希函数,因为在字典查找期间经常调用 isEqual:
。许多项目可能以相同的哈希结束(或在相同的哈希桶中,不确定 NSDictionary 是如何工作的)。
由于某些原因,与许多部分中每个 7 项相比,1 个部分中的所有项目更快。可能是因为它在 NSIndexPath isEqual 上花费了太多时间,如果行首先区分,那会更快,或者 UICollectionViewItemKey 得到更好的哈希。
老实说,UICollectionView 在每个滚动帧都处理繁重的字典,这感觉真的很奇怪,正如之前提到的,每个帧更新需要
对于一般的 UICollectionView 操作确实需要 那里支持一些很少使用的边缘情况,并且可以为大多数布局关闭 一些未优化的内部代码尚未修复 我们这边的一些错误希望我们能在今年夏天的 WWDC 期间看到一些改进,或者如果这里的其他人能弄清楚如何解决它。
【讨论】:
伟大的观察。也许这是值得关注的事情。 当您考虑它时,需要字典查找,因为与表格视图不同,单元格的显示没有保证的顺序。您可以随时定义单元格以遵循任何规则,并且集合视图需要知道任何给定框架的单元格在屏幕上以及它们应该放置在哪里(即使单元格从第 1、2 和 3 部分可见,也有仍然不能保证第 2 节中的所有单元格都在屏幕上)。当您滚动自己的布局时,这一点变得非常清楚。数组不会这样做,因为所需的索引路径通常是不连续的。 Andreas 你在滚动你自己的 Layout 对象吗?我也发现了 NSDictionary 的性能问题,但是我认为当我开始缓存任何生成的 layoutAttributes 对象时,这种情况有所缓解。我注意到如果没有缓存这些会发生不好的事情,因此当将来对布局对象进行请求时,会为给定的 IndexPath 返回完全相同的属性对象。我说 think 是因为不幸的是,我直到一段时间后才注意到该指标的改进,并且还没有运行特定的测试来确认。 有趣的想法,但后来我认为它倾向于未优化的内部代码。集合视图一次向布局询问layoutAttributes
的全屏,然后在内部保存它们。滚动时没有可能导致延迟的数据源或委托调用,只有内部工作。
滚动时只需要查找新可见的矩形中的项目,以及仅在新隐藏的矩形中的项目。这应该能够通过适当的空间数据结构非常有效地完成。在考虑collectionview可以做的所有事情时可能不是,但是对于具有中心,大小和旋转的常规二维网格,使用AABB或其他东西应该很容易。顺便说一句,我使用了默认的流布局。【参考方案4】:
这里是 Altimac's answer 转换为 Swift 3:
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
另外,应该注意的是,此代码位于 cellForItemAtIndexPath 的 collectionView 委托方法中。
另一个提示 - 要查看应用的每秒帧数 (FPS),请在 Instruments 下打开 Core Animation(参见屏幕截图)。
【讨论】:
应该是 UIScreen.mainScreen().scale 而不是 UIScreen.main.scale。 Mat0 你用的是什么版本的 Swift?使用 Swift 3,UIScreen.main.scale 是要走的路…… UIScreen.mainScreen().scale 现在是 Swift 3 之前的“老派”! ;-)【参考方案5】:问题不在于您在集合视图中显示的单元格总数,而在于一次在屏幕上显示的单元格数量。由于单元格尺寸非常小 (22x22),因此您一次可以在屏幕上看到 154 个单元格。渲染其中的每一个都会减慢您的界面速度。您可以通过增加 Storyboard 中的单元格大小并重新运行应用程序来证明这一点。
很遗憾,您无能为力。我建议通过避免裁剪到边界并尝试不实现 drawRect:
来缓解问题,因为它很慢。
【讨论】:
我的意思是单元格/屏幕。应该澄清:)。而且我也有单元格是没有孩子的简单单元格(也没有drawRect),那里有同样的问题。从配置文件看来,集合视图在每个渲染循环上执行所有(可见)属性的副本。这就是 CPU 时间花费的地方,而不是绘图。 drawRect 提示帮助我解决了我的问题。【参考方案6】:对上面的两个答案竖起大拇指!
您可以尝试以下另一件事:通过禁用自动布局,UICollectionView
的性能有了很大的改进。虽然您必须添加一些额外的代码来布局单元内部,但自定义代码似乎比自动布局快得多。
【讨论】:
不幸的是,禁用自动布局对我没有帮助。开启和关闭的性能相同。我在故事板级别将其关闭。你知道是否有可能只为细胞关闭它?它们很简单,不需要任何花哨的布局。 我从来没有绑定过,但这可能会有所帮助:***.com/a/15242064/319019【参考方案7】:在少数情况下,这是由于 UICollectionViewCell 中的自动布局。把它关掉(如果你可以没有它的话)滚动会变得光滑:) 这是一个 iOS 问题,他们已经很久没有解决了。
【讨论】:
谢谢你!你拯救了我的一天!!!!在自定义键盘扩展中禁用单元格 xib 文件中的自动布局给我 60fps 而不是 iPhone 4S iOS 8.1.4 上的 5-10。 这对我波涛汹涌的 iPhone 4S 有很大帮助。对于那些这样做的人,您可以通过将控制器放在它自己的故事板中来仅使用 UICollectionView 禁用视图控制器的自动布局。要轻松地将其放入自己的故事板中,请使用 Interface Builder 中的“Editor->Refactor to Storyboard...”。【参考方案8】:除了列出的答案(光栅化、自动布局等)之外,您可能还需要检查可能会降低性能的其他原因。
就我而言,我的每个 UICollectionViewCell 都包含另一个 UICollectionView(每个大约有 25 个单元格)。当每个单元格都在加载时,我会调用内部 UICollectionView.reloadData(),这会显着降低性能。
然后我将 reloadData 放到主 UI 队列中,问题就消失了:
DispatchQueue.main.async
self.innerCollectionView.reloadData()
仔细研究这些原因可能也会有所帮助。
【讨论】:
【参考方案9】:如果您正在实施网格布局,您可以通过为每个 row
使用单个 UICollectionViewCell
并将嵌套的 UIView
添加到 cell
来解决此问题。我实际上继承了UICollectionViewCell
和UICollectionReusableView
并覆盖了prepareForReuse
方法以删除所有子视图。在collectionView:cellForItemAtIndexPath:
中,我添加了所有subviews
,它们最初是单元格,将它们的框架设置为原始实现中使用的x
坐标,但将它的y
坐标调整为在cell
内。使用这种方法,我仍然可以使用UICollectionView
的一些细节,例如targetContentOffsetForProposedContentOffset:withScrollingVelocity:
,以便在单元格的顶部和左侧很好地对齐。我从 4-6 FPS 到流畅的 60 FPS。
【讨论】:
【参考方案10】:我想我会很快给出我的解决方案,因为我遇到了一个非常相似的问题 - 基于图像的 UICollectionView。
在我工作的项目中,我通过网络获取图像,将其缓存在设备本地,然后在滚动期间重新加载缓存的图像。
我的缺陷是我没有在后台线程中加载缓存的图像。
一旦我将 [UIImage imageWithContentsOfFile:imageLocation];
放入后台线程(然后通过我的主线程将其应用到我的 imageView),我的 FPS 和滚动就会好很多。
如果你还没有尝试过,一定要试一试。
【讨论】:
以上是关于UICollectionView 性能 - _updateVisibleCellsNow的主要内容,如果未能解决你的问题,请参考以下文章
UICollectionView滚动性能使用AFNetworking加载图片
Objective-C UICollectionView 向上/向下滚动