前言:
视图与动画是用户感受很直观的东西.苹果提供了强大的api供我们使用,不用清楚原理,就可以实现大部分需求.但是易用性跟优化就是个矛盾体,就像 ARC 一样,当你没有遇到内存问题的时候用得很爽,一旦遇到了,就要要求你比在用 MRC 的时候更加了解 iOS 的内存机制。UI 亦是如此。所以了解页面渲染的原理还是很有必要的.本文梳理了iOS的界面层次结构,渲染原理及性能优化等相关主题.
一.iOS界面层次结构
iPhone程序以树形结构管理其上的控件,每个视图都置于其父视图上并管理着自己的子视图。程序界面以UIWindow为树根节点,管理所有子视图.UIWindow也是UIView的子类,但是UIWindow并无任何可视化内容,它只负责管理其上的子视图。一般而言,每个应用程序只有一个UIWindow。这种树形结构的优势在于渲染时通过简单的树深度优先原则即可正确渲染界面。另一方面,这样的树形结构在消息传递方面也是很便捷。例如一个触摸事件放生时,UIWindow首先接收到消息,然后将消息传递给响应的子视图,响应子视图又再传给它的子视图。通过这样的层级传递,可以效率较高地找到真正的响应对象.
UIView是iPhone程序非常重要的元素.所有可视化控件都继承于UIView,UIView主要有三方面功能:
渲染区域内容与执行动画
管理子视图
处理触摸、手势等事件
二.界面更新原理
以触摸事件为例从触摸事件的发生到界面改变主要经过八个步骤:
用户触摸屏幕
硬件将触摸事件传递到UIKit框架
UIKit框架将触摸事件打包成UIEvent对象并传递给响应View
View接收到事件后更改视图内容
UIKit发起重新布局视图
UIKit发起重新渲染绘制视图
更改的视图发送给GPU
GPU重新绘制视图并在屏幕中显示
三.产生卡顿的原因
我们看到的视图都是一帧一帧绘制而显示的.而当一帧画面绘制完成后,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。
在需要显示一个视图时,CPU计算好显示内容提交到CPU,CPU渲染完成后将渲染结果放在帧缓冲区,视频控制器按照VSyn信号逐行读取帧缓重区的数据,经过可能的数模转换传递给显示器显示.工作方式如下图所示:
注意:为啥有垂直同步信号,一般情况下使用双缓冲机制可以提高效率。GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象.GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新.
那么为啥会出现屏幕卡顿呢.每次VSync信号到来时显示到屏幕上.由于垂直同步的机制,如果在一个VSync时间内,CPU或GPU没有完成内容提交,则那一帧就会被抛弃.等待下一次机会再显示.而这时显示屏保会保留之前的内容不变.这就是界面卡顿的原因.
具体显示过程如下图所示:
此外我们再了解下CPU和GPU所处的位置
3.1CPU消耗型任务
布局计算
布局计算是常见的消耗CPU的地方.对于视图层级比较复杂的视图,计算出所有图层的布局信息很消耗CPU.
关于优化可以做两点:
1.提前计算好布局信息.
2.避免不必要的更新.
对象创建
对象的创建过程伴随着内存分配.属性设置.甚至还有读取文件等操作,都比较消耗CPU资源.
关于优化可以做3点:
1.使用轻量的对象代替重量对象.eg:在不需要响应触摸事件的视图元素使用用CALayer而不是UIView.
2.使用纯代码创建视图.而不是Storyboard.使用Storyboard创建视图对象会涉及到文件反序列化操作,资源消耗较大
3.对于列表类页面.参考UITableView的复用机制.
对象调整
关于属性调整:
CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。
关于视图层次调整:
UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。
对象销毁
如果容器类有很多对象,那么销毁时也是占用资源的.
优化:
如果对象可以放到后台线程去释放,那就挪到后台线程去.小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。
Autolayout
建议使用手动布局,并控制好视图刷新的时机.使用AutoLayout进行界面开发,大大的提高了开发的速度.但是对于复杂视图来说往往产生严重的性能问题.
文本计算
如果视图中含有大量的文本,那么文本的宽高计算也会很消耗CPU资源.
关于优化:
可以计算好布局信息存储起来,下次使用.
文本渲染
我们能看到的所有文本控件,在底层都是通过CoreText排版,绘制为BitMap显示的.绘制过程在主线程进行,当需要显示文本时,CPU的压力会很大.
具体关系图如下所示:
关于优化:
1.包含文本的视图,在改变布局时会触发文本的重新渲染,因此可以减少静态文本所在视图的布局修改.
2.可以放弃使用上层控件.转而直接使用CoreText进行排版和绘制.
图像的绘制
图像绘制的过程通常使用CoreGarphic提供的api,将图像绘制在画布中,然后从画布创建图片并显示的过程.
关于优化:
可以将绘制过程放在后台线程.然后在主线程将结果设置到layer的contens中.
图片的解码
图片被加载后需要解码,这是个耗时并且占用内存的过程.为了节省内存,系统会延迟解码过程,在图片被设置到layer.content或者imageView.image时才执行解码.这个操作同样是在主线程进行.可能会带来性能问题.
关于优化:
1.可以提前解码.提前将图片绘制到CGContext中.
2.加载Image时,imageNamed加载图片后会马上解码.并且系统会将解码后的图片缓存起来.系统会在合适的时间进行释放.关于优化,可以使用static字段修饰图片避免被释放掉,以空间换时间提高性能.
3.2GPU消耗性任务
GPU主要负责变换.合成和渲染.大多数CALayer的属性都是用GPU来绘制的.
大量几何结构
当图片过大超出CPU最大纹理尺寸,图片需要先由CPU进行预处理,这对CPU和CPU都会带来额外的消耗.
所有的Bitmap,包括图片,文本,栅格化的内容,最终都要由内存提交到显存,绑定为GPU Textture,GPU调整和渲染Texture及提交到显存等操作都需要消耗GPU资源.我们应避免在短时间展示大量图片,这样CPU的占用很低,CPU占用非常高,界面会掉帧.应精良将多张如合成一张图显.
视图的混合
当视图重叠在一起显示时,CPU会首先把他们混合在一起.
关于优化.
应减少视图数量和层次,减少不必要的透明视图,降低GPU的消耗.
离屏渲染
离屏渲染是指图层在被显示之前是在当前屏幕缓冲区以外开辟的一个缓冲区进行渲染操作。(离屏渲染发生是一种预处理,它为要显示的下一帧处理,这里说明一下)
离屏渲染需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文环境从离屏切换到当前屏幕,而上下文环境的切换是一项高开销的动作。
会造成 offscreen rendering 的常见原因有:
阴影(UIView.layer.shadowOffset/shadowRadius/…)
圆角(当 UIView.layer.cornerRadius 和 UIView.layer.maskToBounds 一起使用时)
优化:
使用阴影时同时设置 shadowPath 就能避免离屏渲染大大提升性能.
圆角触发的离屏渲染可以用 CoreGraphics 将图片处理成圆角来避免
四、CoreText
4.1基础框架
先了解下CoreText的基础框架
CTFrame可以想象成画布, 画布的大小范围由CGPath决定
CTFrame由很多CTLine组成, CTLine表示为一行
CTLine由多个CTRun组成, CTRun相当于一行中的多个块, 但是CTRun不需要你自己创建, 由NSAttributedString的属性决定, 系统自动生成。每个CTRun对应不同属性
CTFramesetter是一个工厂, 创建CTFrame, 一个界面上可以有多个CTFrame
4.2基本使用
CoreText的基本使用
//1.获取当前上下文
let context = UIGraphicsGetCurrentContext()
//2.转换坐标系
CGContextSetTextMatrix(context, CGAffineTransformIdentity)
CGContextTranslateCTM(context, 0, self.bounds.size.height)
CGContextScaleCTM(context, 1.0, -1.0)
//3.初始化路径
let path = CGPathCreateWithRect(self.bounds, nil)
//4.初始化字符串
let attrString = NSMutableAttributedString(string: "Hello CoreText")
//5.初始化framesetter
let framesetter = CTFramesetterCreateWithAttributedString(attrString)
//6.绘制frame
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, nil)
CTFrameDraw(frame, context!)
4.3 创建简单的富文本label
CoreText的基本使用
//将整个frame绘制改为按行绘制
//1.获得CTLine数组
let lines = CTFrameGetLines(frame)
//2.获得行数
let numberOfLines = CFArrayGetCount(lines)
//3.获得每行的origin.
var lineOrigins = [CGPoint](count: numberOfLines, repeatedValue: CGPointZero)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins)
var lineOrigins = [CGPoint](count: numberOfLines, repeatedValue: CGPointZero)
//4.遍历每一行进行绘制
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins)
//还可以改成按run绘制等....
五.Instruments 使用
Color Blended Layers
这个选项选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮显示,越红表示性能越差,会对帧率等指标造成较大的影响。红色通常是由于多个半透明图层叠加引起。
绿色区域是没有混合渲染的,红色区域是经过有混合渲染的。
每次遇到这样的情况,你都应当仔细研究,并在不同的情况下,使用不同的方式来避免混合渲染。在一般情况情况下,将背景色设置不透明就已经实现目标了。
Color Offscreen-Rendered Yellow
这个选项会把那些离屏渲染的图层显示为黄色。黄色越多,性能越差。这些显示为黄色的图层很可能需要用 shadowPath 或者 shouldRasterize 来优化。
六.UITableView的优化
6.1cell复用
cell复用是TableView的核心机制.在此不再赘述.
6.2cell高度的计算
cell的高度设置有两种方法,一种是定高的设置:self.tableView.rowHeight = 88;这种方法适合设置高度固定的cell.并且不需要再调用代理方法,可以节约计算和开销.另一种是通过代理动态设置cell的高度.通过代理方法实现后,上面的rowHeight的设置将会变得无效.
值得注意的是,heightForRowAtIndexPath这个方法会被多次调用,假设有50个cell,屏幕上只显示3个,那么heightForRowAtIndexPath这个方法就会调用50次,cellForRow方法调用3次,接下来滚动屏幕,每个cell进入再调用一次heightForRow方法,一次cellForRow方法.
因此适当优化这个方法中的计算是很有必要的.另一方面也可以通过缓存高度,减少计算的次数.
6.3渲染
提高渲染速度可以从3方面着手:
1.利用预渲染加速显示iOS图像:在bitmap context先将其画一遍.导出成UIImage对象,然后再绘制到屏幕.
2.渲染最好的操作之一是使用混合.不要使用透明背景,将cell的opaque值设置为yes,背景色不要使用clearColor,不要使用阴影渐变等,尽量规避离屏渲染.
3.在UIView的drawRect方法中自定义绘制.由于混合操作是使用GPU来执行,我们可用CPU渲染,
6.4其他
1.减少视图的数目:减少添加的视图,推荐集成cell,重写drawRect方法
2.减少多余的绘制操作:只绘制rect范围内的区域
3.在初始化cell的时候就将所有需要展示的控件添加完毕,根据需求设置hide控制显示和隐藏,减少对视图层级的更改.
4.滑动时按需加载对应的内容:如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。
参考: