移动app中,列表页面非常常见,apple为我们提供了UITableView和UICollectionView这两大组件来帮我们执行这样的任务。server根据请求的page返回对应分页数据,然后app这边请求到数据之后,更新数据源并刷新界面,中间请求的时候,我们可以使用MJRefresh这样的第三方来反映当前的状态,是请求中还是已经请求完毕,或者是已经没有更多的数据了。
但是在网络比较差的时候偶,app向server请求数据到获取到数据并更新界面的时间会很长,非常影响用户体验,为了改善这种情况,在iOS10上,apple引入了Prefetching API,提供了一种在需要显示数据之前预先准备数据的机制,旨在提高数据的滚动性能。
正常情况下,在构建UITableView的时候,需要对其行数做一个初始化,但是如果每次根据服务端返回的数据去更新行数并reload的时候,Prefetching API就失去作用了,它起作用的前提是要保证预加载数据时候,UITableView当前的行数要小于其总行数,当然也可以实现数据加载,不过跟我们预期效果不一致。
也就是说,我们在分页请求的时候,需要server提供当前的数据以及总的数据量。在这里,我们模拟一下数据请求,从server请求30张图片

    func fetchImages() {
        guard !isFetchInProcess else {
            return
        }
        
        isFetchInProcess = true
        // 延时 2s 模拟网络环境
        DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 2) {
            DispatchQueue.main.async {
                self.total = 1000
                self.currentPage += 1
                self.isFetchInProcess = false
                let imagesData = (1...30).map {
                    ImageModel(url: baseURL+"\($0).png", order: $0)
                }
                self.images.append(contentsOf: imagesData)

                if self.currentPage > 1 {
                    let newIndexPaths = self.calculateIndexPathsToReload(from: imagesData)
                    self.delegate?.onFetchCompleted(with: newIndexPaths)
                } else {
                    self.delegate?.onFetchCompleted(with: .none)
                }
            }
        }
    }

这里通过代理在controller更新view内容

    func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?) {
        guard let newIndexPathsToReload = newIndexPathsToReload else {
            tableView.tableFooterView = nil
            tableView.reloadData()
            return
        }
        
        let indexPathsToReload = visibleIndexPathsToReload(intersecting: newIndexPathsToReload)
        indicatorView.stopAnimating()
        tableView.reloadRows(at: indexPathsToReload, with: .automatic)
    }
    
    func onFetchFailed(with reason: String) {
        indicatorView.stopAnimating()
        tableView.reloadData()
    }

然后,我们实现一下UITableViewDataSourcePrefetching的协议,这个协议中包含两个函数

public protocol UITableViewDataSourcePrefetching : NSObjectProtocol {

    
    // indexPaths are ordered ascending by geometric distance from the table view
    @available(iOS 2.0, *)
    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath])

    
    // 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:
    @available(iOS 2.0, *)
    optional func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath])
}

前者会基于当前滚动的方向和速度对接下来的indexPaths进行Prefetch,通常我们在这里实现预加载数据的逻辑。
第二个方法是当用户快速滚动导致一些cell不可见的时候,可以通过这个方法来取消任何挂起的数据加载操作,有利于提高滚动性能。
然后,我们实现这两个方法,让预加载的图片异步加载并且设定一下缓存。
如果想再进一步优化的话,我们可以在cellForRowIndexPath方法中实例化cell,然后在willDisplayCell方法中进行数据绑定,后者在cell展示之前会被调用,此时cell实例已经生成,不能再修改cell结构了,但是可以改动cell上的UI组件的一些属性。
Tips:
这里写出一般的列表优化思路:
1、就是上文提出的cell复用过程中,cellForRowIndexPath实例化cell,willDisplayCell绑定数据。
2、减少view数目。大量绘制UI组件会消耗很大的资源并影响渲染性能。所以可以在初始化cell的时候将其所有内容初始化完毕,在显示的时候通过显示隐藏来显示不同的效果。
3、异步操作。网络数据请求、图片加载等耗时操作可以将其放入子线程中操作,转会主线程刷新UI即刻。
4、预加载等。