使用iOS 10预提取API增强平滑滚动
在上一篇文章中,我们探讨了一些常见的策略来实现iOS移动应用程序中的平滑滚动。 应用这些策略的主要目的是避免混乱的滚动 ,这是对用户体验产生负面影响的常见问题。 为了帮助开发人员完成这项任务,Apple对iOS10中的UICollectionView进行了一些非常有用的更改。 但是,在审查此新引入的功能之前,让我们先检查促使他们需要这些功能的原因。
是什么导致滚动不稳定?
您是否曾经与某个偶尔出现断断续续的滚动的应用程序进行过交互或工作过? 如果答案是“是”,那么当您尝试快速滚动并且应用程序内容似乎断断续续时,您就会知道它多么令人失望。 您可能已经问过自己,是什么触发了这种不稳定的滚动行为以及随之而来的不良用户体验。
简短的答案是:该应用丢帧了 。 但这到底是什么意思?
为了确保一致的平滑滚动,应用程序需要能够稳定显示60 FPS(每秒帧数)。 或者,换句话说,应用程序需要每秒刷新其内容60次。 这意味着每个帧大约要渲染16ms(1000ms / 60帧〜16ms /帧)。 在不幸的情况下,显示一帧所花费的时间比分配的时间长,下一帧将不会显示任何数据,并且据说该应用程序“ 丢弃了一帧” 。 下图说明了这种不幸的情况。 蓝色标记表示绘图操作,其厚度表示完成渲染所需的时间。 如我们所见,在第二帧中,我们发生了一些渲染事件,这些事件花费的时间超过了分配的时间(〜16ms),因此,第三帧已被删除。
我们可以从刷新操作所花费的CPU时间的角度来可视化同一场景。 在下图中,尖峰对应于当应用花费超过预期的〜16ms来刷新当前内容时发生的丢帧现象。
为了获得良好的用户体验,刷新时间必须始终低于允许的最大值〜16ms。 理想情况下,由于我们希望创造出色的用户体验(而不仅仅是一个好的体验),因此每次刷新时间应为:
- 始终低于允许的最大时间(〜16ms)。
- 尽可能地低,以节省可用于其他任务的CPU时间。
丢失帧的最常见来源是从主线程为单元加载昂贵的数据模型。 这种情况的典型示例是:
- 从URL加载图像。
- 从数据库或CoreData访问项目。
在iOS10中,Apple对单元格的加载和显示方式进行了一些优化。 让我们看一下iOS10可用的改进,以及它们如何使开发人员更轻松地创建流畅的滚动用户体验。
iOS9中的单元生命周期
UICollectionViewCell的生命周期可以如下所示:
集合视图及其单元之间的主要交互是:
- 收集视图正在请求要显示的单元格的内容,该单元格将要输入可见字段:
collectionView(_:cellForItemAt:)
。 - 集合视图要求显示该单元格- 该单元格刚刚输入了可见字段:
collectionView(_:willDisplay:forItemAt:)
。 - 集合视图正在删除单元格- 该单元格在可见字段之外 :
collectionView(didEndDisplaying:forItemAt:)
。
iOS10中的单元生命周期
在iOS10中,单元的生命周期与在iOS9中基本相同。 但是,有一些显着差异。
第一个区别是,操作系统比以前更早调用collectionView(_:cellForItemAt:)
。 这意味着两件事:
- 可以在需要显示单元格之前完成繁重的单元格加载工作。
- 该单元格可能根本不会显示出来(因为它可能永远不会被带入可见区域)。
第二个区别是当细胞离开可见场时会发生什么。 在iOS10中,通常会调用collectionView(didEndDisplaying:forItemAt:)
,但不会立即回收该单元格。 如果用户反转滚动方向,则操作系统会将其保留一点时间。 如果发生这种情况,该单元格仍然可用并且可以再次显示(将调用collectionView(_:willDisplay:forItemAt:)
),而不必重新加载其内容。
将此与iOS9上发生的情况进行比较。 您会注意到,对于此特定用例,不再需要重新加载单元格( collectionView(_:cellForItemAt:)
)。 这种优化使我们可以在用户快速滚动和更改滚动方向时更快地渲染单元格。
iOS9和iOS10之间的第三个重要区别在于,为具有多列布局的集合视图加载单元格的方式。
在iOS10中,有关输入可见字段的每个单元格都会单独加载( collectionView(_:cellForItemAt:)
)。 正如我们在检查单元格生命周期时所看到的,这比实际需要显示每个单元格的时间要早得多。 这为优化打开了方便之门,因为操作系统将能够处理不同的请求,使它们与单元负载交织在一起。
当一行单元格进入可见字段时,这些单元格将显示为一个批处理(同时为整行调用collectionView(_:willDisplay:forItemAt:)
),因为此操作的费用并不高CPU周期(至少与加载单元内容相比)。
加载多列布局的这种差异是最重要的iOS10 UICollectionView优化的核心: Pre-Fetching 。 让我们更详细地探索它。
预取API
在介绍iOS10时,Apple宣称预取是一种自适应技术 。 这意味着预取将尝试利用用户与应用程序的交互方式来执行一些旨在提高滚动性能的优化。 例如,这项新技术将寻找空闲时间(当用户缓慢滚动或完全不滚动时)以加载( 预提取 )新的单元格。 根据用户的滚动模式, 预取可能会有更多(或更少)的机会执行这种优化。
在查看可用的API之前,让我们看一些使用此技术的最佳实践。 为了充分利用预取功能 ,必须在collectionView(_:cellForItemAt:)
执行大部分设置单元格内容的操作。 在collectionView(_:willDisplay:forItemAt:)
和collectionView(didEndDisplaying:forItemAt:)
应保持最少,并且必须不collectionView(didEndDisplaying:forItemAt:)
CPU。 更好的是,如果我们对于这些生命周期事件根本不执行任何操作,那将是最佳选择! 另外,请记住,即使为某个单元collectionView(_:cellForItemAt:)
,该单元格也有可能永远不会显示。
一些非常好的消息是,默认情况下,iOS10上编译的应用程序会启用预提取 。 但是,可以通过将isPrefetchingEnabled
属性设置为false
来关闭此功能。 还需要注意的是, 预取与单元生命周期一起工作。 这意味着我们已经编写的用于实现集合视图的代码无需更改-充分利用Pre-Fetching所需的唯一操作是实现Pre-Fetching API 。
预提取API和UICollectionView
UICollectionView的Pre-Fetching API在UICollectionViewDataSourcePrefetching
协议中定义。 API定义了以下两种方法。
collectionView(_:prefetchItemsAt:)
(必需)—此方法允许启动[IndexPath]
参数指定的单元格所需的数据的异步加载。 可以通过Grand Central Dispatch或借助于OperationQueue
来执行异步加载。 实现这些方法时,最重要的是编写将数据加载负担从主队列转移到后台队列的代码。 这样做的目的是减少主队列的工作量,使其可以将大部分时间花费在执行关键任务上,如显示刷新。
collectionView(_:cancelPrefetchingForItemsAt:)
(可选)—此方法表明不再需要[IndexPath]
参数指定的单元格数据。 实现此方法使我们可以根据需要取消挂起的数据加载,这是通过取消不必要的工作(通常是因为用户更改了滚动方向)来节省CPU时间的好方法。
如前所述, 预取是一种自适应技术 。 因此,上述方法是基于用户与应用程序交互的方式触发的。 这样的结果是,可能不会为集合视图中的每个单元collectionView(_:prefetchItemsAt:)
用collectionView(_:prefetchItemsAt:)
方法。 这意味着,当通过collectionView(_:cellForItemAt:)
加载单元格时,该应用程序应能够处理以下所有情况:
- 数据已被预取并准备显示。
- 当前正在获取数据,尚未准备好显示。
- 尚未请求数据。
预提取API和UITableView
iOS10还为UITableView引入了Pre-Fetching 。 我们为UICollectionView阐述的所有主要概念都以与UITableView相似的方式应用。 UITableView的预提取API在UITableViewPrefetchingDataSource
协议中定义。 API定义了以下两种方法。
tableView(_:prefetchRowsAt:)
(必需)—此方法允许启动[IndexPath]
参数指定的单元格所需的数据的异步加载。
tableView(_:cancelPrefetchingForRowsAt:)
(可选)—此方法表明,不再需要[IndexPath]
参数指定的单元格的数据。
针对每种UICollectionView 预提取API方法提出的建议以相同的方式应用于每种模拟UITableView 预提取API方法。
结论
我已经更新了UITableView和UICollectionView的代码示例,以支持iOS10上的预提取 。 你可以在这里找到它。
在本文中,我们回顾了iOS10可用的改进,以促进UICollectionView和UITableView的平滑滚动。 特别是,我们看到,通过实现特定的Pre-Fetching API ,可以在与我们的移动应用程序进行交互时充分利用所有新的OS优化功能,并提供最佳的用户体验。
有关
- Swift中的引用和值类型
- UITableView和UICollectionView中的平滑滚动
- Swift中的通用数据源
- 通用协议类型擦除的替代方法