iOS 性能优化-让UITableView如丝般顺滑(布局、离屏
UITableView 是 iOS 中最常用的组件之一,也是承载业务最重,交互最多的组件之一。对 UITableView 的优化最能体现开发人员在性能优化上的基础水平,直接关系到我们的用户体验。
还记得第一次创建空白的列表么?那如少女肌肤般的顺滑令人终身难忘。可时间的推移、业务的增长,列表中展示的东西越来越多,少女变大妈,不再顺滑,满是褶皱...
???
这.........
我们绝不接受,我们要大妈返老还童!
快上车,来不及解释了。
为什么会卡顿?
与游戏类似,游戏中的 FPS 通常可以用来代表当前的流畅程度。当 FPS 小于30帧每秒时人眼就会感觉到画面不够连续,有撕裂感。这也是所有游戏必须保证 FPS 在任何情况下都维持在30以上的原因,是不影响玩家游戏体验的最低标准。我们的APP也是同理的。
无论是游戏,还是程序应用,用户看到的都是一张张连续的图片。基于人眼的特点,当画面达到一定刷新率时,人眼就会认为其是连续的,把这认为是欺骗我们的眼睛也不为过。
高刷新率对硬件和程序性能有着苛刻的要求,但是过低的刷新率会给用户造成不适,当刷新率降低到 24 以下时,人眼就能明显的感觉到画面的卡顿/不连贯。无论何时,刷新率都应当保持在 30 以上。
为什么我们的画面帧率会降低到30以下?
苹果的屏幕帧率很早就达到60了,单位时间内帧率降低到30以下,说明某些帧绘制超时了。主要原因如下:
1.复杂的事务
2.低效的绘制
复杂的事务优化起来需要因地制宜。例如网络请求、复杂的任务队列。这些问题大多可以使用多线程,优化业务流程解决。
低效绘制
造成低效绘制的原因有很多,常见的有三点:
- 复杂的界面布局
- 离屏渲染
- 繁重的绘制
1.复杂的界面布局
在 iOS 中,界面布局无非 Autolayout 和 Frame 两种方式,XIB,SB 都是对 Autolayout 的更高级的可视化封装。
(SwiftUI未来一年还将处于试水的阶段,这里暂不讨论)。
Frame布局的优点在于:程序不需要翻译约束来计算视图的位置,直接绘制即可。
采用Frame布局的方式时,视图的位置大小信息由开发人员编写计算完成。低效的计算代码也可能会影响到布局的速度,尽可能采取简单的计算方式,避免循环、迭代、事务等待的情况发生。
将视图的布局信息提前到编码阶段,对开发人员的水平、工作量、及规范程度的要求都会极大的提高,容错性也越低,若代码不够规范,维护起来简直就是地狱。
Autolayout 会通过各种约束计算转换成最终的视图位置参数,过程是机械式的,越是复杂的界面就越可能耗费更长的时间。但其胜在容错性高、可视化、快速开发、易于调试等优点。
(小结):
界面越是简单,布局方式影响性能的几率就越低,而程序只要在当前帧完成绘制,就不存在任何问题。当界面越来越复杂,布局的影响就必须重视。时间充裕的情况下,应当尽可能降低”翻译约束“的发生。
在开发中,也需要权衡,花费大量的时间去提升0.01秒的速度是否值得。
2.离屏渲染
【什么是离屏渲染?】
程序在屏幕以外的地方生成了一块用于绘制的缓冲区域,当前帧在缓冲区域绘制完成后,直接将结果显示到屏幕上,这整个过程称作离屏渲染。离屏渲染可以防止画面撕裂等情况发生,在游戏等视频类领域大量使用。
【iOS 中为什么会有这项技术?】
在屏幕绘制的过程中,很多时候并不能一次性得到最终的显示效果,如:有“透明度”的视图,有“遮罩”效果的图层,有“圆角”的图片等等。
我们从屏幕上看到的这些效果都是计算机对各视图或图层的属性混合后的最终效果,而这个 “计算混合“ 的过程就需要另开辟一块缓冲区域进行绘制,即“离屏渲染”,离屏渲染完成后将结果复制到屏幕上。
(小结):
离屏渲染并不是什么Bug,它是计算机图像绘制过程中非常重要的一项技术。之所以会被列入优化的清单,因其会 创建缓冲区 域用于计算图像的混合结果,比较耗费性能。
当一个界面大量出现这样需要使用离屏渲染技术时,消耗的系统资源也就越多。尤其是类似 UITableView 这类组件滚动时,离屏渲染所占用的系统资源会呈指数上升,影响用户体验。
在可能频繁更新的组件或区域中尽可能的杜绝使用到离屏渲染,防止在某些的情况下(如:快速滚动)突破性能峰值。
3.大量的绘制
无论我们布局多么简单,又或是我们避免了离屏渲染。我们还是会遇上渲染性能问题。例如:
- 图片显示问题
- 大量的绘制
关于图片,可以分为大图显示,多图加载等。关于绘制,又可以分为动效动画,多图形绘制。这里就不展开讨论图片的绘制原理,网上有很多讲解的博客。
(小结)
默认情况下,绘制都是由GPU负责,但在图形和图片的绘制过程中,可以利用 CPU 帮忙负担一部分开销的。
优化原理总结:
性能优化的本质是防止超过性能峰值的情况发生,并尽可能的降低系统资源消耗的程度。
具体优化手段
布局优化
因布局的卡顿导致的问题主要出现在视图初次显示时,例如跳转到新页面时、Cell初次出现时。
在对性能或开发速度没有明确的要求时,合理采用不同的布局手段,在效率和性能中做好平衡,例如,在复杂的页面布局中适当降低 Autolayout 的占比。
可以的话,将界面的子视图延后加载,先让界面显示出来,再来加载子视图。
离屏渲染
离屏渲染发生在图层混合时,尽可能避免离屏渲染在高频率显示的地方出现。
可以将某些图形绘制交由CPU完成,给GPU减负,降低性能峰值.
例如: 在 drawRect: 中使用 CoreGraphics 绘制图形、圆角等。
有时候,不得不利用到离屏渲染。如我们的Cell中有一个阴影效果,可以将第一次混合出来的结果 缓存 起来给其他Cell使用,避免重复渲染。
Cell 的复用
在 UITableView 中,苹果给我们提供了 “复用” 功能,让程序仅生成刚好够显示数量的 Cell,通过复用 Cell 来避免创建无穷无尽 Cell 的惨案发生。
复用 Cell 的过程中要避免频繁修改Cell布局的情况发生。复用Cell的过程中修改布局相当于重新计算绘制一次,极易影响到滑动的流畅性。正确做法应当是对不同的布局注册为不同的标识符(Identifier).这种会动态更改布局的 Cell 不能进行提前注册(register(nib:forCellReuseIdentifier:)),而是需要对不同的布局分别进行注册,如下:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row < 10 {
var cell = tableView.dequeueReusableCell(withIdentifier: TestCellID)
if cell == nil {
cell = TestCell(style: .default, reuseIdentifier: TestCellID)
}
return cell
}
var cell = tableView.dequeueReusableCell(withIdentifier: TestCellSmallID)
if cell == nil {
cell = TestCell(style: .default, reuseIdentifier: TestCellSmallID)
//这里各种修改布局
}
return cell
}
动态高度
Cell 不总是等高的,当我们的高度为动态高度时,在 Cell 的复用过程中,高度的更新意味着复用 Cell 的约束需要更新,进而 Cell 的绘制也需要更新。这里面就还牵扯到了Cell 本身的布局方式,以及高度的获取效率。
在 iOS8 之前,开发者常会将Cell的布局信息缓存起来,不用每次重新计算。
从 iOS8 开始,苹果在 UITableView 中加入了基于 Autolayout 的自动高度属性
rowHieght = UITableViewAutomaticDimension
嗯哼~
在前面布局的部分提到过,Autolayout 在复杂的布局中性能会明显低于 Frame。更何况是在会频繁复用的动态高度的 Cell 中。
所以,动态高度时我们需要做到两点:
- Cell 越是复杂,越需要注意因布局导致的性能问题(Frame > Autolayout)
- 将高度计算结果缓存起来进行复用,避免不必要的重复计算。
耗时的操作(图形绘制、资源加载等)
UITableView 展示过程中不可避免的会加载某些资源,如图片、音频、视频。这些资源从加载到解码再到显示出来都比较耗费时间,单纯的加载显示我们可以通过多种方式减缓性能峰值:
- 对资源进行压缩,降低分辨率、码率等方式,毕竟列表中大多时候只是一个预览效果,不需要全长全质加载显示。
- 异步执行事务,在子线程中处理,防止负责绘制的主线程出现等待状态。
- 分解单个事务, 以时间换取空间(性能),降低性能峰值。
例1:在滚动时,我们并不需要 cell 立刻显示出内容,尤其是在高速滚动时,我们可能根本就不需要 cell 显示。这时候通过维护一个事务队列来管理cell的状态很有必要。
例2:上面提到的大图加载,同样可以将单次加载分解为多个部分.
性能优化没有最好,只有最适合,具体情况具体分析,奇淫技巧各显神通~
时刻留意性能,是一个优秀的开发者必备的特质,哪怕是在硬件性能越来越强的今天。
“还想要返老还童的大妈?还是觉得不够顺滑?
Too young too native!”
帅气的笔者已经焊死了车门,并撕开了自己的衬衣...