跳转到主要内容

【iOS】TableView的优化

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

原创文章2025iOS 底层iOSUITableView性能优化【iOS】TableView的优化
作者 TommyWu
本文 2,281 字,阅读预计需要 12 分钟
目录展开
封面图片: 【iOS】TableView的优化

一、优化的本质

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

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

二、卡顿产生原因

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

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

三、CPU 层面的优化

cellForRowAtIntexPath 方法不要进行耗时操作

1、不读取 / 写入文件

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

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

cell 的复用

简单来讲,UITableView 会按需配置当前可见或即将显示的行,并按 reuseIdentifier 复用离屏 cell;复用池大小不是固定的 “屏幕 cell 数 + 1”。

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

可以参考我之前的博客:

自定义 cell 与 cell 的复用

提前计算布局

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

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

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

estimatedRowHeight

动态行高通常使用完整的 Auto Layout 约束,并设置 rowHeight = UITableViewAutomaticDimension;estimatedRowHeight 提供预估值,帮助表格延后部分几何计算。

Auto Layout 求解约束有计算成本,但不能因此断言它必然增加内存开销;是否构成瓶颈应结合视图规模测量。

我们通过 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。

estimatedRowHeight 是行高预估值,不存在 estimatedRowHeight:AtIndexPath 这一逐行计算方法,也不会先为全部数据计算实际行高。实际行高由固定高度、heightForRowAtIndexPath: 或 self-sizing 布局确定;UITableView 按需为即将显示的行配置 cell。

请添加图片描述

2、预先计算并缓存行高

在这里插入图片描述

实现 heightForRowAtIndexPath: 时,UIKit 会为每一行(包括屏幕外行)请求高度;昂贵的计算可能影响性能。estimatedRowHeight 可延后部分几何计算,不应把特定版本的调用顺序写成保证。

异步加载图片:SDWebImage 的使用

(1)使用异步子线程处理,然后再返回主线程操作; (2)图片缓存处理,避免多次处理操作; (3)shouldRasterize 会先把图层栅格化成 bitmap 再参与合成,可能以缓存和内存换取重复合成成本;它不是把负载转移给 CPU 的开关,也不是图片圆角的通用优化。

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

四、GPU 层面优化

离屏渲染

UIKit 与 Core Animation 的实际渲染路径由系统负责,不能概括为当前 iOS 底层必然使用 OpenGL ES;Apple 已将 OpenGL ES 标记为弃用。一般可从 CPU 准备界面内容、GPU 执行渲染与合成来理解,但具体路径取决于系统实现。

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

  • 离屏渲染是否发生取决于图层效果与系统实现,不能按版本简单断言所有圆角都会触发。单独设置 cornerRadius 不一定触发离屏渲染;与裁剪(masksToBounds / clipsToBounds)组合时可能需要中间渲染目标。

以下情形可能增加额外绘制、混合或离屏渲染成本;是否发生取决于具体图层内容与系统实现:

  • 设置 layer.mask 等遮罩可能需要额外合成。
  • masksToBounds / clipsToBounds 与圆角等裁剪组合时可能触发离屏 pass;不能把单独圆角一概判为离屏渲染。
  • allowsGroupOpacity、阴影和 shouldRasterize 的额外 pass 或缓存成本取决于图层内容及系统实现。
  • 文本和 drawRect: 自定义绘制本身不代表必然离屏渲染;应通过 Instruments 或 Core Animation 调试选项核实。

离屏渲染的优化

** 使用贝塞尔曲线 + 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.image drawInRect: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 通过 path 表示形状;path 可由 UIBezierPath 构造,也可来自其他 CGPath。 CAShapeLayer 的具体渲染路径、离屏 pass 与缓存成本由图层内容和系统实现决定,不能仅凭 drawRect: 或图层类型断言由 CPU/GPU 完成或性能更高。 CAShapeLayer 作为图片 mask 可能增加合成成本;应在目标设备上与绘图上下文或 cornerRadius 方案实测比较。

不要把 CAShapeLayer/贝塞尔曲线当作通用性能或内存优化;应在目标列表中 profile 后选择方案。

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

总结

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

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

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

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

接着阅读