iOS10 - PrefetchDataSource 详解

8,870 阅读7分钟
原文链接: redeemd.github.io

前言

最近在写自己的 V2EX 客户端,想要按 ObjC中国更轻量的 View Controller 这篇文章中说的,把 dataSource 相关的操作独立出来,翻 API 的时候看到了 iOS 10 新加入的 UITableViewDataSourcePrefetch 协议,出于好奇就查了一些资料

发现这次主要是 Apple 针对于 CollectionView 大量 Item 快速滑动时掉帧严重问题的优化。

PrefetchDataSource

以下内容打多翻译自:Collection View Updates in iOS10, Part 1

在 iOS 10 中除了一大堆全新的特性,还有一些对现有特性的调整,UICollection View 就是其中之一

即使 UICollectionView 已经是一个拥有很高性能的控制器,Apple 仍没有停止优化它。最令人感兴趣的新特性之一,就是对于大量沉重的数据源时 collection views 表现性能的提高。

背景

任何 iOS 应用的目标都是让 UI 跑满60帧, 以达到一个完全平滑滚动的效果。任何帧数明显低于标准值的情况,都会通过掉帧和迟钝的表现性能展示出来。

如果你处理绘制 collection view 时用的数据源速度不够快的话,60fps 相当于每帧16.67ms 的时间并不是很充足。获得高性能 collection views 的诀窍是让 cellForItemAtIndexPath 方法尽快的返回一个 cell,但这可能并不容易。比如异步加载图片这样的技术会有一些帮助,但并足以称之为灵丹妙药。

Note: This technique is also available to use with UITableView - just replace references to UICollectionView with UITableView, and the implementation patterns are identical.

注意:这个技术对于 UITableView 同样有效,只需要将 UICollectionView 的引用替换成 UITableView,然后实现的模式是一样的

Prefetching

在 iOS 10 中,Apple 引入了一个新的 UICollectionViewDataSource 协议扩展 —— UICollectionViewDataSourcePrefetching

这同样为 UICollectionView 引入了一个新的属性 —— prefetchDataSource 。这个类实现了 UICollectionViewDataSourcePrefetching 协议的两个方法:

  • collectionView:prefetchItemsAtIndexPaths:
  • collectionView:cancelPrefetchItemsAtIndexPaths:

Apple 文档中的定义大概如下:(因为 UITableViewDataSourcePrefetching 的文档比较详细,就贴了它的….)

// _______________________________________________________________________________________________________________
// this protocol can provide information about cells before they are displayed on screen.
@protocol UITableViewDataSourcePrefetching 
@required
// indexPaths are ordered ascending by geometric distance from the table view
- (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray *)indexPaths;
@optional
// indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -tableView:prefetchRowsAtIndexPaths:
- (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray *)indexPaths;
@end

为了能及时地传递 cell,当 collection view 的滚动速度将要超过 cellForItemAtIndexPaths 的能力时,collection view 会调用 prefetchItemsAtIndexPaths 方法

Collection view 传递了以后可能需要的 cell 的 NSIndexPaths 数组,这使你有机会去更新 collection view 的基础数据源。例如,如果你的数据源是一个图片 数组,你可以在这个方法中提前调用网络下载这些图片并把他们插入到数据源中,这样他们就可以直接被 cellForItemsAtIndexPaths 方法使用了。

第二个方法通常在滚动方向发生改变的时候被调用。这么做的原因是:你可能在一些 cell 显示之前就已经提前加载好了数据,如果此时界面反向滚动他们就不是即将现实的状态了,所以你可以取消任何正在进行的数据源更新操作。同样,上面讨论的这些 cell 的 NSIndexPaths 也会作为一个 数组 传递。

Implementing pre-fetching

想要利用新的预加载特性的优势,你需要做到以下四条:

  • 令一个类遵从 UICollectionViewDataSourcePrefetching 协议
  • 实现 collectionView:prefetchItemsAtIndexPaths 方法,来更新 collection view 的 dataSource
  • (可选) 实现 collectionView:cancelPrefetchItemsAtIndexPaths 方法,来取消正在执行的预加载操作
  • 把遵从协议的类设置为 collectionViewprefetchDataSource 属性

遵从 UICollectionViewDataSourcePrefetching 协议

这并不难,只需要标记你的数据源实现了该协议

@interface RDXViewController : UIViewController 
// something you want
@end

实现预加载功能

预加载通过 collectionView:prefetchItemsAtIndexPaths 方法实现,这里 collection view 传递了一个 NSIndexPaths 数组,包含了每一个即将显示的 cell 所对应的索引路径。

你要做的就是对整个数组进行遍历,然后相应地更新 collection view 的数据源

- (void)tableView:(UITableView *)tableView prefetchItemsAtIndexPaths:(NSArray *)indexPaths {
    
    for (NSIndexPath *indexPath in indexPaths) {
    
        .... expensive operation to retrievesome data ....
        
        dataSource[indexPath.row] = retrievedData;
        
    }
}

这里有两点需要着重注意:

  • 第一点是你应该 更新 collection view 的底层数据源,而不直接返回任何数据。这可以向更新一个字符串数组一样简单(如上),或者更复杂一些,比如更新一个 CoreData 或者 Realm Model。
  • 第二点要牢记的是,在预加载开始之后,谁也不能保证接收的数据何时或是否会被使用。Collection view 的滚动速度可能会降下来;或者滚动方向完全相反。

出于这个原因,对于时间敏感的数据可能在它显示的时候已经过时了。你是否需要考虑这个因素当然取决于将要显示的这个数据的性质。

实现取消预加载功能

从一定角度来看,collection view 的预加载请求只是试图优化未来不确定状态的一种猜测,这种状态可能并不会真实发生。例如,如果滚动速度放缓或者完全反转方向,那些已经请求过的预加载 cell 可能永远都不会确切地显示。

在这种情况下,任何正在执行的预加载操作都将会是白费力气。与其做一些冗余的请求,UICollectionViewDataSourcePrefetch 协议直接定义了 collectionView:cancelPrefetchingForItemsAtIndexPaths: 这个方法来取消还未完成的请求。

这个方法将在 collection view 判断当前预加载操作是冗余优化任务时被调用。它用一个参数来传递 NSIndexPaths 数组 ,然后由你来遍历这些数组并取消任何有必要结束的请求。

关联 collection view 的预加载代理

在遵从了协议并实现了一个或两个方法之后,你需要把这个类添加为 collection view 的 prefetchDataSource

这和给属性复制一样简单(这里,我们假定 collection view controller 本身就是自己的 prefetchDataSource

collectionViewController.prefetchDataSource = self;

这必须在 UICollectionViewDataSource 的任何协议方法调用完成,这样他就会与设置 collection view 的其他属性(如注册 cell)同时生效。

总结

UICollectionView 本来就是一个极高性能的控制器,它做了大量我们看不到的性能优化。在 collection view cells 要展示的源数据获取负担较大或速度较慢的特殊情况下,预加载技术是弥补性能不足甚至获取更好性能的另一个工具。

然而,在代码中有这么一个说法,”过早的优化是万恶之源”,所以这并不是一个 magic bullet(一种未发现或假设的药物,拥有极好且非常明确的特征,这里引申为十分优异的解决方案。我没有找到合适的对应词….)。并且一个不容忽视的事实就是,这个过程中负荷最大的部分往往就是合成 collection view cell 本身。但如果你想要挤出最后一滴性能以换取collection view 的滑动尽可能得平滑,预加载可能值得一试。

后记

这篇文章中内容比较简单,主要是为了方便那些可能读英文文档真的捉急的小伙伴和一些初学者了解下 iOS 10 新增的预加载 API 相关的流程。
虽然简单但篇幅不算短的,初次翻译这种也废了比较多的时间….
如果对你有所帮助还请支持一下我~~
你就是单纯的想支持一下我也可以,但目前来看可能还没有办法~~
可能,帮忙转发扩散?(碎碎念)