tableView性能优化
动态高度
我们需要实现它的代理,来给出高度:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// return xxx
}
复制代码
这个代理方法实现后,上面的rowHeight的设置将会变成无效。在这个方法中,我们需要提高cell高度
的计算效率,来节省时间。
自从iOS8
之后有了 self-sizing cell
的概念,cell
可以自己算出高度,使用self-sizing cell
需要满足以下三个条件:
(1)使用 Autolayout
进行 UI
布局约束(要求cell.contentView
的四条边都与内部元素有约束关系)。
(2)指定 TableView
的 estimatedRowHeight属性
的默认值。
(3)指定 TableView的rowHeight
属性为 UITableViewAutomaticDimension
。
- (void)viewDidload {
self.myTableView.estimatedRowHeight = 44.0;
self.myTableView.rowHeight = UITableViewAutomaticDimension;
}
复制代码
除了提高cell高度
的计算效率之外,对于已经计算出的高度,我们需要进行缓存,对于已经计算过的高度,没有必要进行计算第二次。
减少视图的数目
我们在 cell
上添加系统控件的时候,实际上系统都会调用底层的接口进行绘制,大量添加控件时,会消耗很大的资源并且也会影响渲染的性能。当使用默认的 UITableViewCell
并且在它的 ContentView
上面添加控件时会相当消耗性能。所以目前最佳的方法还是继承 UITableViewCell
,并重写drawRect方法
。
重绘操作仍然在 drawRect方法
中完成,但是苹果不建议直接调用 drawRect方法
,当然如果你强直直接调用此方法,当然是没有效果的。苹果要求我们调用UIView类中的 setNeedsDisplay方法
,则程序会自动调用 drawRect方法
进行重绘。(调用 setNeedsDisplay
会自动调用 drawRect
)。
UITableView的圆角性能优化
- 让服务器直接传圆角图片;
- 贝塞尔切割控件
layer
; YYWebImage
为例,可以先下载图片,再对图片进行圆角处理,再设置到cell
上显示
使用 shadowPath 来画阴影
阴影触发离屏渲染的原因在于需要显示在所有layer
内容的下方,因此必须被渲染在先。但此时阴影的本体(layer
和其子layer
)都还没有被组合到一起,只能另外申请一块内存,把本体内容都先画好,再根据渲染结果的形状,添加阴影到 帧缓冲区frame buffer
,最后把内容画上去。不过如果我们能够预先告诉 CoreAnmation
(通过shadowPath属性
)阴影的几何形状,那么阴影当然可以先被独立渲染出来,不需要依赖layer本体,也就不再需要离屏渲染了。
透明度不为1
设置了组透明度为YES
,并且透明度不为1
的 layer
(不透明)
产生离屏渲染的条件是 layer.opacity != 1.0
并且有子 layer
或者背景图。alpha
并不是分别应用在每一层之上,而是只有到整个 layer树
画完之后,再统一加上 alpha
,最后和底下其他 layer
的像素进行组合。显然也无法通过一次遍历就得到最终结果。
Prefetching API
在 viewDidLoad
中先请求网络数据来获取一些初始化数据,然后再利用 UITableView
的 Prefetching API
来对数据进行预加载,从而来实现数据的无缝加载。
UITableViewDataSourcePrefetching 协议
// this protocol can provide information about cells before they are displayed on screen.
@protocol UITableViewDataSourcePrefetching <NSObject>
@required
// indexPaths are ordered ascending by geometric distance from the table view
- (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)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<NSIndexPath *> *)indexPaths;
@end
复制代码
第一个函数会基于当前滚动的方向和速度对接下来的 IndexPaths
进行 Prefetch
,通常我们会在这里实现预加载数据的逻辑。
第二个函数是一个可选的方法,当用户快速滚动导致一些 Cell
不可见的时候,你可以通过这个方法来取消任何挂起的数据加载操作,有利于提高滚动性能, 在下面我会讲到。
实现这俩个函数的逻辑代码为:
extension ViewController: UITableViewDataSourcePrefetching {
// 翻页请求
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount}
if needFetch {
// 1.满足条件进行翻页请求
indicatorView.startAnimating()
viewModel.fetchImages()
}
for indexPath in indexPaths {
if let _ = viewModel.loadingOperations[indexPath] {
return
}
if let dataloader = viewModel.loadImage(at: indexPath.row) {
print("在 \(indexPath.row) 行 对图片进行 prefetch ")
// 2 对需要下载的图片进行预热
viewModel.loadingQueue.addOperation(dataloader)
// 3 将该下载线程加入到记录数组中以便根据索引查找
viewModel.loadingOperations[indexPath] = dataloader
}
}
}
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){
// 该行在不需要显示的时候,取消 prefetch ,避免造成资源浪费
indexPaths.forEach {
if let dataLoader = viewModel.loadingOperations[$0] {
print("在 \($0.row) 行 cancelPrefetchingForRowsAt ")
dataLoader.cancel()
viewModel.loadingOperations.removeValue(forKey: $0)
}
}
}
}
复制代码
最后,再加上俩个有用的方法该功能就大功告成了:
// 用于计算 tableview 加载新数据时需要 reload 的 cell
func visibleIndexPathsToReload(intersecting indexPaths: [IndexPath]) -> [IndexPath] {
let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? []
let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths)
return Array(indexPathsIntersection)
}
// 用于确定该索引的行是否超出了目前收到数据的最大数量
func isLoadingCell(for indexPath: IndexPath) -> Bool {
return indexPath.row >= (viewModel.currentCount)
}
复制代码
如何避免滚动时的卡顿: 异步化UI,不要阻塞主线程
当你遇到滚动卡顿的应用程序时,通常是由于任务长时间运行阻碍了 UI
在主线程上的更新,想让主线程有空来响应这类更新事件,第一步就是要将消耗时间的任务交给子线程去执行,避免在获取数据时阻塞主线程。
苹果提供了很多为应用程序实现并发的方式,例如 GCD
,我在这里对 Cell
上的图片进行异步加载使用的就是它。
代码如下:
class DataLoadOperation: Operation {
var image: UIImage?
var loadingCompleteHandle: ((UIImage?) -> ())?
private var _image: ImageModel
private let cachedImages = NSCache<NSURL, UIImage>()
init(_ image: ImageModel) {
_image = image
}
public final func image(url: NSURL) -> UIImage? {
return cachedImages.object(forKey: url)
}
override func main() {
if isCancelled {
return
}
guard let url = _image.url else {
return
}
downloadImageFrom(url) { (image) in
DispatchQueue.main.async { [weak self] in
guard let ss = self else { return }
if ss.isCancelled { return }
ss.image = image
ss.loadingCompleteHandle?(ss.image)
}
}
}
// Returns the cached image if available, otherwise asynchronously loads and caches it.
func downloadImageFrom(_ url: NSURL, completeHandler: @escaping (UIImage?) -> ()) {
// Check for a cached image.
if let cachedImage = image(url: url) {
DispatchQueue.main.async {
print("命中缓存")
completeHandler(cachedImage)
}
return
}
URLSession.shared.dataTask(with: url as URL) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let _image = UIImage(data: data)
else { return }
// Cache the image.
self.cachedImages.setObject(_image, forKey: url, cost: data.count)
completeHandler(_image)
}.resume()
}
}
复制代码
那具体如何使用呢!别急,听我娓娓道来,这里我再给大家一个小建议,大家都知道 UITableView
实例化 Cell
的方法是:tableView:cellForRowAtIndexPath:
,相信很多人都会在这个方法里面去进行数据绑定然后更新 UI
,其实这样做是一种比较低效的行为,因为这个方法需要为每个 Cell
调用一次,它应该快速的执行并返回重用 Cell
的实例,不要在这里去执行数据绑定,因为目前在屏幕上还没有 Cell
。我们可以在 tableView:willDisplayCell:forRowAtIndexPath:
这个方法中进行数据绑定,这个方法在显示cell
之前会被调用。
为每个 Cell
执行下载任务的实现代码如下:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "PreloadCellID") as? ProloadTableViewCell else {
fatalError("Sorry, could not load cell")
}
if isLoadingCell(for: indexPath) {
cell.updateUI(.none, orderNo: "\(indexPath.row)")
}
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// preheat image ,处理将要显示的图像
guard let cell = cell as? ProloadTableViewCell else {
return
}
// 图片下载完毕后更新 cell
let updateCellClosure: (UIImage?) -> () = { [unowned self] (image) in
cell.updateUI(image, orderNo: "\(indexPath.row)")
viewModel.loadingOperations.removeValue(forKey: indexPath)
}
// 1. 首先判断是否已经存在创建好的下载线程
if let dataLoader = viewModel.loadingOperations[indexPath] {
if let image = dataLoader.image {
// 1.1 若图片已经下载好,直接更新
cell.updateUI(image, orderNo: "\(indexPath.row)")
} else {
// 1.2 若图片还未下载好,则等待图片下载完后更新 cell
dataLoader.loadingCompleteHandle = updateCellClosure
}
} else {
// 2. 没找到,则为指定的 url 创建一个新的下载线程
print("在 \(indexPath.row) 行创建一个新的图片下载线程")
if let dataloader = viewModel.loadImage(at: indexPath.row) {
// 2.1 添加图片下载完毕后的回调
dataloader.loadingCompleteHandle = updateCellClosure
// 2.2 启动下载
viewModel.loadingQueue.addOperation(dataloader)
// 2.3 将该下载线程加入到记录数组中以便根据索引查找
viewModel.loadingOperations[indexPath] = dataloader
}
}
}
复制代码
对预加载的图片进行异步下载(预热):
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount}
if needFetch {
// 1.满足条件进行翻页请求
indicatorView.startAnimating()
viewModel.fetchImages()
}
for indexPath in indexPaths {
if let _ = viewModel.loadingOperations[indexPath] {
return
}
if let dataloader = viewModel.loadImage(at: indexPath.row) {
print("在 \(indexPath.row) 行 对图片进行 prefetch ")
// 2 对需要下载的图片进行预热
viewModel.loadingQueue.addOperation(dataloader)
// 3 将该下载线程加入到记录数组中以便根据索引查找
viewModel.loadingOperations[indexPath] = dataloader
}
}
}
复制代码
取消 Prefetch
时,cancel
任务,避免造成资源浪费
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){
// 该行在不需要显示的时候,取消 prefetch ,避免造成资源浪费
indexPaths.forEach {
if let dataLoader = viewModel.loadingOperations[$0] {
print("在 \($0.row) 行 cancelPrefetchingForRowsAt ")
dataLoader.cancel()
viewModel.loadingOperations.removeValue(forKey: $0)
}
}
}
复制代码