【iOS】TableView的优化

一、优化的本质 UITableView 的优化本质在于提高滚动性能和减少内存使用,以保证流畅的用户体验,从计算机层面来讲,其核心本质为降低 CPU和GPU 的工作来提升性能 CPU:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制 G

作者 TommyWu
封面圖片: 【iOS】TableView的优化

#一、优化的本质

UITableView 的优化本质在于提高滚动性能和减少内存使用,以保证流畅的用户体验,从计算机层面来讲,其核心本质为降低 CPU 和 GPU 的工作来提升性能

CPU:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制 GPU:接收提交的纹理和顶点描述、应用变换、混合并渲染、输出到屏幕

#二、卡顿产生原因

App 主线程在CPU中显示计算内容,比如视图的创建,布局的计算,图片解码,文本绘制,然后我们的 CPU 会将计算好的内容提交到GPU中进行变换,合成,渲染,这其中也包括我们常说的离屏渲染

在开发中,CPU与GPU任何一个压力过大都会导致掉帧

#三、CPU 层面的优化

#cellForRowAtIntexPath 方法不要进行耗时操作

1、不读取 / 写入文件

2、尽量少用 addView 给 Cell 动态添加 View,可以在初始化就添加,然后通过设置气 hidden 来控制是否显示

因为我们在滑动UITableView的过程中会不断调用这个方法。

#cell 的复用

简单来讲就是我们的UITableView只会创建比一个屏幕所有显示的 cell + 1 个单元格,当当前的 cell 划出屏幕时,我们的 cell 并不会销毁,而是会将其存入我们的复用池

当要显示某一个位置的 cell 时首先会去复用池中查找,如果找不到才会重新创建,而不是每一次都进行重新创建,这样就极大地减少了内存开销

可以参考我之前的博客:

自定义 cell 与 cell 的复用

#提前计算布局

解了这件事情与 tableView 的复用机制之后,我们再回头看我们的 cellForRow 与 heightForRow 方法,我们知道当我们滑动 tableview 时就不断地调用这两个方法,因此这两个方法是性能优化的关键

UITableViewCell 高度计算主要分为两种,一种固定高度,另外一种动态高度

rowHeight 这个属性是我们的固定高度,对于定高需求的表格,强烈建议使用这种方法来避免不必要的高度计算以及调用。或者我们可以使用UITableViewDelegateheightForRowAtIndexPath,然而,实现这个方法后我们的 rowHeight 将无效,所以这个方法适合具有多种 cell 的 UITableView。

estimatedRowHeight

这是一个估算行高的属性,对于动态计算行高,这里有多种方法,但核心还是通过设置预算高度和estimatedRowHeight = UITableViewAutomaticDimension,然后用AutoLayout对控件进行约束达到撑开cell的目的。

但是这也不可避免地加大了内存开销,因为 AutoLayout 最终需要转成 frame

我们通过 estimatedRowHeight 与 AutoLayout 大大简化了我们动态计算行高的过程,同时我们需要尽可能精确估计 estimatedRowHeight 的范围,即使面对种类不同的 cell,我们依然可以使用简单的 estimatedRowHeight 属性赋值,只要整体估算值接近就可以,比如大概有一半 cell 高度是 44, 一半 cell 高度是 88, 那就可以估算一个 66,基本符合预期。尽可能精确的估算可以使初次加载和滚动表格时更加流畅

#1、设置预估行高

我们知道UITableView是通过 UITableView 代理方法heightForRowAtIndexPath:方法来设置行高。自从 iOS8.0 之后,苹果新增了 self-sizing cell 的概念,也是 cell 可以自己计算行高,使用需要满足是三个条件:

(1) 使用Autolayout进行 UI 布局约束 (2) 指定TableView的estimatedRowHeight属性的默认值 (3) 指定 TableView 的 rowHeight 的属性为UITableViewAutomaticDimension

TableView 在加载数据时会先通过estimatedRowHeight:AtIndexPath处理全部数据,此时我们只需要提供一个粗略的高度,待到 cell 对象创建之后再去设置 cell 的真实高度。而且只会处理当前屏幕范围内的 cell,这样子会显著的提升加载的性能。

请添加图片描述

#2、预先计算并缓存行高

在这里插入图片描述

从上图可以很容易的分析出,iOS8.0 之后在获取 cell 对象之后会再次调用heightForRowAtIndexPath:方法获取行高,这也就意味着我们其实可以先创建 cell 对象,之后再提供行高。具体方法我们可以在 cell 类中添加 layoutAttribute 属性,记录相应的UIEdgeInsets,然后在设置 cell 真实高度的时候返回。iOS7.0 之前则必须在 cell 对象处啊给你讲爱你之前先获得所有 cell 的高度。

#异步加载图片:SDWebImage 的使用

(1)使用异步子线程处理,然后再返回主线程操作; (2)图片缓存处理,避免多次处理操作; (3)图片圆角处理时,设置 layer 的 shouldRasterize 属性为 YES,可以将负载转移给 CPU;

这部分内容笔者现在也不是很清楚,会在学习网 SDWebImage 源码后再进行补充。

#四、GPU 层面优化

#离屏渲染

什么是离屏渲染?我们知道 iOS 底层的渲染框架使用的是OpenGL ES。OpenGL 中,GPU 渲染屏幕方式有两种:当前屏幕渲染(On-Screen Rendering)和离屏渲染(Off-Screen Rendering)。它们的区别是当前屏幕渲染操作是在当前屏幕缓冲区完成,而离屏渲染会在另外一个新开辟的缓冲区完成渲染操作,开启离屏渲染的代价就是需要新开辟一块新的缓冲区,在渲染的过程中还会多次切换上下文,这些都是很消耗性能的。

官方对离屏渲染产生性能问题也进行了优化:

  • iOS 9.0 之前UIimageView跟UIButton设置圆角都会触发离屏渲染。
  • iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。

一下情况均会造成 离屏渲染 :

  • 为图层设置遮罩(layer.mask)
  • 设置图层的 layer.masksToBounds / view.clipsToBounds 属性为 True
  • 设置图层的 layer.allowsGroupOpacity 的属性为 True 和 layer.opacity 小于 1.0
  • 设置图层阴影(layer.shadow)
  • 设置图层的 layer.shouldRasterize 的属性为 True - 具有 layer.cornerRadius,layer.edgeAntialiasingMask, – layer.allowsAntialiasing 的图层
  • 文本(任何种类,包括 UILabel、CATextLayer、Core Text 等)
  • 使用 CGContext 在 drawRect

#离屏渲染的优化

** 使用贝塞尔曲线 + Core Graphics 框架设置圆角 **

- (void)setImageCircularEdge:(UIImageView *)imageView {
//开始对imageView进行画图
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
//使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
//结束画图
UIGraphicsEndImageContext();
}

** 使用贝塞尔曲线 + CAShapeLayer 设置圆角 **

- (void)setImageCircularEdge2:(UIImageView *)imageView {
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer=[[CAShapeLayer alloc] init];
//设置大小
maskLayer.frame = imageView.bounds;
//设置图形样子
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
}

对于方案 2 需要解释的是:

CAShapeLayer继承于CALayer,可以使用 CALayer 的所有属性值; CAShapeLayer需要贝塞尔曲线配合使用才有意义(也就是说才有效果) 使用 CAShapeLayer (属于 CoreAnimation) 与贝塞尔曲线可以实现不在 view 的 drawRect(继承于 CoreGraphics 走的是 CPU, 消耗的性能较大)方法中画出一些想要的图形 CAShapeLayer 动画渲染直接提交到手机的 GPU 当中,相较于 view 的 drawRect 方法使用 CPU 渲染而言,其效率极高,能大大优化内存使用情况。

总的来说就是用CAShapeLayer的内存消耗少,渲染速度快,建议使用优化方案 2。

对于离屏渲染的检测,据说苹果为我们提供了一个测试工具,这里先埋一个坑,以后再来填。

#总结

UITableView 的性能优化涉及到了许多层面,下到底层的 Layer 属性,上到第三方库 SDWebImage 与 RunLoop,这些东西的实现都十分巧妙,还有很长一段路需要学习

这里笔者写一下 Tableview 性能优化方法总览

  • 实现 Tableview 的懒加载以及 cell 的复用(这是优化 Tableview 最基础的部分,老生常谈了,特别是复用池这一块的内容:将加载过的 cell 加入到复用池中,需要时取出)
  • 高度缓存(本篇文章着重介绍了 cell 的高度缓存机制,因为 heightForRowAtIndexPath: 是调用最频繁的方法,我们围绕这个方法展开,通过避免重复计算来减少我们的内存开销。当行高固定时使用固定行高,不固定时缓存一次后返回固定行高)
  • 预缓存(在高度缓存的实现上进行优化,涉及到 RunLoop 层面的知识,后面加以补充,似乎与 SDWebImage 有异曲同工之处,十分巧妙)
  • 异步加载图片(SDWebImage 的使用,后面看源码看完再回来总结)
  • 按需加载内容(涉及到许多协议,当快速滑动时不加载资源,即将停止滑动时加载资源,同时释放那些超出屏幕的资源,只显示目前呈现在屏幕上的内容)
  • 视图层面(避免离屏渲染,使用贝塞尔曲线或是直接呈现采裁剪后的圆角图片来避免离屏渲染的发生)

原文发布于 CSDN:【iOS】TableView 的优化