UITableView性能优化
前言
笔者近期刚刚结束第二个项目,了解学习学长博客以及自己搜索资料,发现UITableView
有很多可以进行优化的部分,本篇博客对其性能优化进行一个总结,主要从CPU
以及GPU
两个方面出发,去解决性能优化的问题,以提高用户使用的流畅度。
CPU
:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制。GPU
:接收提交的纹理和顶点描述、应用变换、混合并渲染、输出到屏幕。
优化从何入手
- 提前计算并缓存好高度(布局),因为
tableView:heightForRowAtIndexPath:
是调用最频繁的方法; - 滑动时按需加载,防止卡顿。这个在大量图片展示,网络加载的时候很管用,配合
SDWebImage
; - 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
- 缓存一切可以缓存的,这个在开发的时候,往往是性能优化最多的方向。
需要关注的点:
cell
的复用- cell高度的计算
- 渲染(混合问题)
- 减少视图数量(重写
drawRect:
) - 减少多余的绘制操作
- 不要给cell动态添加
subView:
- 异步化UI,不要阻塞主线程
- 滑动时按需加载对应的内容。
优化的本质
UITableView
的优化本质在于提高滚动性能和减少内存使用,以保证流畅的用户体验,从计算机层面来讲,其核心本质为降低CPU
和GPU
的工作来提升性能
CPU层级优化
1. Cell的复用
在UITableView
中最核心的就是UITableViewCell
的复用机制。简单来说,即UITableView只会创建比一屏幕多一点的cell,其余的都是从复用池中取出来重用的。当显示某一个位置的时候,都是先从复用池中取,如果存在就直接拿来显示,如果不存在,才重新创建。
在了解了cell的复用原理后,我们看看 UItableView
的回调方法,最主要的两个回调方法是:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
有时候我们会认为是先调用第一个height
再调用cellForRowAtIndexpath
方法的,但实际上是先调用cellForRowAtIndexpath
再调用height
方法的。笔者另一篇博客详细讲解了相关内容:【iOS】自定义cell及其复用机制
2. 尽量少定义Cell,善于使用hidden控制显示视图
分析cell结构,尽可能将相同内容的抽取到一种样式Cell中,虽然可能Cell的体积会大一点,但是Cell的数量并不会很多,完全还是可以接受的。
- 基于复用机制,运行时铺满屏幕的Cell数量大致是固定的,设为N,所以cell的实例为(N+c)个;但是如果有M个Cell的话,cell的实例就最多可能为M*(N+c)个了,虽然可能并不会多占用多少内容,但是少一点还是会好一点。
- 减少代码量,减少 Nib 文件的数量,在一个 Nib 文件定义 Cell,容易修改、维护
尽量少在 cellForRowAtIndexPath:
方法中使用[cell addSubView:]
动态添加View,多使用hidden属性来控制view的隐藏和展示。同样,也不要在该方法中读取文件/写入文件
3. 提前计算并缓存高度
UITableView
的代理方法执行顺序
如果我们设定了预估行高estimatedRowHeight
,我们的Tableview
会每一次先调用cellForRow
又调一次对应的heightForRow方法。
如果我们并没有设定estimatedRowHeight
,则会先遍历一次每个cell的tableView:heightForRowAtIndexPath:
获取总的高度值 ,然后每调用一次cellForRow
的时候再调用一次对应的heightForRow
。
但因为iOS11中的tableView的estimatedRowHeight
默认值由原来的0变为UITableViewAutomaticDimension
(打印出来为-1),因此我们可以大致认为我们每一次先调用cellForRow
后会再调一次对应的heightForRow方法。
也就是说我们的Tableview
一定是先调用cellForRow
方法,然后再调用heightForRow
方法。
Cell高度
在我们使用UITableView
的时候,其询问Cell高度有两种不同的方式:
rowHeight
:
这种方式是针对所有Cell都具有固定高度的情况,设置的时候是:self.tableView.rowHeight = 100;
来进行设置,不去实现tableView:heightForRowAtIndexPath:
方法,减少了不必要的开销和计算,当我们遇到定高的cell的时候,我们可以使用这种方法,我们需要注意的是Cell的默认高度为44。- 使用
tableView:heightForRowAtIndexPath:
方法:
通过使用协议中的这个方法,会使我们设置的rowHeight
变得无效,这点我们需要注意,但同时,我们可以更便捷的设置多个不同Cell的高度。
estimatedRowHeight
:
这个属性首次出现在iOS7.0,在官方文档中,描述为:
If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
与上面设定Cell的高度相同,设置估算行高也有两种方式,还有一种即为使用函数: - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
。但是当我们面对不同的Cell的时候,还是可以只使用 estimatedRowHeight
来设置。例如:当我们设置的时候,一个Cell的高度为40,另一种Cell的高度为80,我们就可以设置成60,尽量减少计算来使得第一次加载的时候更加流畅。
缓存高度数组
这里我们可以创建一个数组来存储已经计算好的cell高度,当他第一次显示的时候判断计算高度,并存入数组中,后面直接取出来使用即可。
4. 异步绘制
即异步操作在画布上绘制内容,将复杂的绘制过程放在后台线程中执行,而后在主线程显示:
笔者的GCD学习并不到位,后期笔者重新学习GCD后再回来补充这里的问题。
5. 滑动时按需加载
当我们自定义Cell的时候,多数情况下我们需要网络申请获取图片,虽然使用SDWebImage
会异步加载图片,但是当我们开启线程过多的时候,也会卡顿,尤其是当网络不好的时候,所以我们需要利用UIScrollViewDelegate
中的两个代理方法 scrollViewDidScroll:
和 scrollViewDidEndDecelerating:
方法来判断滚动状态。在 scrollViewDidScroll:
中暂停加载图片,这样可以很好的解决这个问题。
我们还需要注意的一个问题是,当我们的Cell超出目标范围后,我们应该清除掉他们:当用户滑动表格或集合视图时,只为屏幕范围内可见的单元格(cell)加载内容。一旦这些单元格滚动出屏幕变得不可见,与它们关联的资源(如图像数据)应该被清除或释放,以避免不必要的内存占用。尽量减少内容的消耗。
6. 使用异步加载图片,减少主线程的消耗
对于使用异步加载图片,我们可以直接使用SDWebImage
这个第三方库来加载图片。
这里笔者还没有了解该库后再进行了解。
7. 使用轻量化对象
我们一般来说设置画面的时候都使用的是UIView
,但是其实有些地方不需要事件处理的时候我们可以使用CALayer
,相比来说,CALayer
更加接近底层一点。在UIView
中其实也有CALayer
属性,直接使用CALayer
可以减少UIView
层级的额外计算(当没有事件响应的时候)
GPU层面
控制尺寸
GPU能处理的最大纹理尺寸为4096*4096,超过这个尺寸就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸。
纹理是一种在 GPU(图形处理单元)中用于表示图像或其他数据的二维数组。它可以被看作是一种特殊的图像数据结构,主要用于在渲染过程中为物体表面添加细节和颜色信息。这里笔者页不是很清楚,后期学会了再进行补充。
减少图层混合操作
当多个视图叠加,放在上面的视图是半透明的,那么这个时候GPU就要进行混合,把透明的颜色加上放在下面的视图的颜色混合之后得出一个颜色显示在屏幕上,这一步是消耗GPU资源。
UIView
的backgroundColor
不要设置为clearColor
,最好设置和superView
的backgroundColor
颜色一样。
图片避免使用带alpha通道的图片
避免离屏渲染
GPU主要负责渲染与绘制图形,因此在这个层面我们着重讲一下iOS中的离屏渲染对UITableView性能产生的影响
下面的情况或操作会引发离屏渲染
- 光栅化,
layer.shouldRasterize = YES
- 遮罩,
layer.mask
- 圆角,同时设置
layer.masksToBounds = YES
和layer.cornerRadius > 0
- 阴影,
layer.shadow
layer.allowsGroupOpacity = YES 和 layer.opacity != 1
- 重写drawRect方法
优化建议
- 使用中间透明图片蒙上去达到圆角效果
- 使用ShadowPath指定layer阴影效果路径
- 使用异步进行layer渲染
- 将UITableViewCell及其子视图的opaque属性设为YES,减少 - 复杂图层合成
- 尽量使用不包含透明alpha通道的图片资源
- 尽量设置layer的大小值为整形值
- 背景色的alpha值应该为1,例如不要使用clearColor
- 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
- 很多情况下用户上传图片进行显示,可以让服务端处理圆角