原理
-
显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号,水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync
图片.png
就是一个垂直信号,就代表一帧动画
- 苹果采用的是双缓冲区机制,Gpu会预先渲染一帧放到一个缓冲区中,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。
- 双缓冲带来的问题,就是视频控制器第一帧还没有显示完 的时候,Gpu将指针指向了第二个缓冲区,就会把新一帧的下半段显示出来,会造成图像撕裂
- 解决这一问题,就是等垂直信号来的时候,再去进行缓存区的更换,即指针的指向,避免一个没结束显示第二个,这样会降低效率
CPU、GPU各自承担的工作
- cpu
Layout:UI布局 文本计算
Display:绘制(drawRect)
Prepare:图片编解码
Commit:提交位图
- GPU工作
图层的合成 ,纹理渲染,GPU渲染管线的过程:实际上这个过程指的就是OpenGL的渲染管线
渲染5步:
渲染结束之后,就会把最终的像素点,提交到对应的帧缓冲区中。
UI卡顿和掉帧的原因
- 页面流畅一般是60fpts(苹果目前是),就是一秒需要产生60帧的数据,这样页面看起来就会很流畅,也就相当于每16.7ms需要在cpu和Gpu的合作下生成一帧数据;
- 举例:文本显示,cpu先计算文本的布局,绘制出文本,然后交给GPU去渲染,这一轮操作结束,就能正常生成一帧数据,如果cpu的处理时间很长,留给GPU的渲染时间很短,可能没渲染完成的时候,下一个垂直信号vsync到来时 ,显示器拿不到数据,就会出现掉帧,也就是卡顿的现象
- 总结成一句话,就是在规定的16.7ms之内,在下一帧VSync信号到来之前,并没有CPU和GPU共同完成下一帧画面的合成,这就会造成卡顿或者说掉帧,Cpu的处理,Gpu的渲染,都可能造成卡顿
滑动优化方案
知识准备
像素是如何出现在屏幕上的:
从左往右看
- GPU Driver:GPU驱动软件,直接和 GPU 交流的代码块
- OpenGL:提供了 2D 和 3D 图形渲染的 API,高GPU的能力,并实现硬件加速渲染,是第一个和图形硬件(GPU)交流的标准化方式,界面图像的渲染都是通过openGL,具体看上面的openGL 5步渲染
- Core Graphics:Quartz 2D的一个高级绘图引擎,常用与iOS,tvOS,macOS的图形绘制应用开发。Core Graphics是对底层C语言的一个简单封装,其中提供大量的低层次,轻量级的2D渲染API。【前缀为CG,比如常见的CGPath,CGColor】
- Core Animation:是苹果提供的一套基于绘图的动画框架,但不止是动画,他同样是绘图的根本【前缀是CA,比如CALayer】,core animation 里面也有对core Grapthics的一些封装
- Core Image:iOS处理图像的框架,主要用处可以给图片添加滤镜效果和图像识别功能。
最底层是图形硬件(GPU),通过CPU Driver 来调度;上层是OpenGL和CoreGraphics,提供一些接口来访问GPU,在上面是core animation 和core image,处理动画,图形,在上面就是UIKit
像素
即RGB,位图数据有时候被称为RGB数据;
alpha,透明度,透明度直接乘以rgb对应的值
图形合成
多个图层重叠之后,需要统一各个图层的rgb然后算出最后一个展示的rgb值来进行最后展示渲染
透明与不透明:
当源纹理是完全不透明的时候,目标像素就等于源纹理。这可以省下 GPU 很大的工作量,这样只需简单的拷贝源纹理而不需要合成所有的像素值。但是没有方法能告诉 GPU 纹理上的像素是透明还是不透明的。这也是为什么 CALayer 有一个叫做 opaque 的属性了。如果这个属性为 YES,GPU 将不会做任何合成,而是简单从这个层拷贝,不需要考虑它下方的任何东西(因为都被它遮挡住了)。这节省了 GPU 相当大的工作量。
如果你加载一个没有 alpha 通道的图片,并且将它显示在 UIImageView 上,会自动设置opaque 为 YES。
对齐与不对齐
如果几个图层的模版都是完美重合,那我们只要从第一个像素到最后一个像素都计算合成一下,但是如果像素没有对齐好,我们还需要额外进行额外的移位操作,合并原纹理上的像素
两种情况会导致不对齐出现: 缩放,当纹理的起点不在一个像素的边界上
离频渲染
- 在屏渲染(On-Screen Rendering):指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
- 离屏渲染(Off-Screen Rendering):指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
用通俗的语言总结一下:当我们在设置某些UI视图的图层属性,如果说指令为在未预合成之前,不能用于直接显示的时候呢,那么就触发了离屏渲染。
离屏渲染的概念起源于GPU,那GPU层面上呢,如果在当前屏幕缓冲区之外新开辟一个缓冲区去进行渲染操作的话呢,那么就是离屏渲染。
何时会触发离屏渲染
圆角(当和maskToBounds一起使用,即该属性设为YES时才会触发离屏渲染,缺一不可)
图层蒙版
阴影
光栅化:隐式创建一个位图,各种阴影遮罩等效果也会保存到位图中缓存起来,从而减少渲染的频度,把GPU的操作转到CPU上,生成位图缓存,直接读取调用。(注:对于经常变动的内容,不要开启光栅化,防止性能浪费,如Cell的复用)
为何要避免离屏渲染?
CPU 和 GPU 在做具体的渲染过程中做了大量的工作,而离屏渲染是发生在 GPU 层面上面的,使 GPU 层面上面触发了 OpenGL 多通道渲染管线,产生了额外的开销,所以需要避免离屏渲染。
标准回答:
在触发离屏渲染的时候,会增加 GPU 的工作量,而增加 GPU 的工作量很有可能会到导致CPU和GPU工作总耗时超出了16.7ms,那么可能就会导致UI的卡顿和掉帧,那么我们就要避免离屏渲染。
另一种回答:
会创建新的渲染缓冲区,会有内存上的开销
会有上下文的切换,因为有多通道渲染管线,要把多通道的渲染结果进行一个合成,那么就有GPU一个额外的开销。
- 离屏渲染的检测:Instruments的CoreAnimation工具动态监测。(使用方法:Color Offscreen –Rendered Yellow :开启后会把那些需要离屏渲染的图层高亮成黄色,黄色图层可能存在性能问题。)
- 光栅化的检测:Color Hits Green and Misses Red 开启后,若shouldRasterize设置为YES,对应的渲染结果会缓存,如果图层是绿色,表示缓存被复用;如果是红色,就表示缓存被重复创建,可能存在性能问题。
滑动优化分析
CPU资源消耗分析
1、对象创建:对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗CPU资源。尽量采取轻量级对象,尽量放到后台线程处理,尽量推迟对象的创建时间。(如UIView / CALayer)
2、对象调整:frame、bounds、transform及视图层次等属性调整很耗费CPU资源。尽量减少不必要属性的修改,尽量避免调整视图层次、添加和移除视图。
3、布局计算:随着视图数量的增长,Autolayout带来的CPU消耗会呈指数级增长,所以尽量提前算好布局,在需要时一次性调整好对应属性。
4、文本渲染:屏幕上能看到的所有文本内容控件,包括UIWebView,在底层都是通过CoreText排版、绘制为位图显示的。常见的文本控件,其排版与绘制都是在主线程进行的,显示大量文本是,CPU压力很大。对此解决方案唯一就是自定义文本控件,用CoreText对文本异步绘制。(很麻烦,开发成本高)
5、图片解码:当用UIImage或CGImageSource创建图片时,图片数据并不会立刻解码。图片设置到UIImageView或CALayer.contents中去,并且CALayer被提交到GPU前,CGImage中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。SD_WebImage处理方式:在后台线程先把图片绘制到CGBitmapContext中,然后从Bitmap直接创建图片。
6、图像绘制:图像的绘制通常是指用那些以CG开头的方法把图像绘制到画布中,然后从画布创建图片并显示的一个过程。CoreGraphics方法是线程安全的,可以异步绘制,主线程回调。
GPU资源消耗分析
1、纹理混合:尽量减少短时间内大量图片的显示,尽可能将多张图片合成一张进行显示。
2、视图混合:尽量减少视图层次和数量,并在不透明的视图里标明opaque属性以避免无用的Alpha通道合成。
3、图形生成:尽量避免离屏渲染,尽量采用异步绘制,尽量避免使用圆角、阴影、遮罩等属性。必要时用静态图片实现展示效果,也可尝试光栅化缓存复用属性。
4、 比如视图层级十分复杂,那GPU需要合成每一个对应像素点的像素值,做大量的计算,这个合成过程也会变得复杂。减轻视图层级的复杂性,会减轻GPU合成视图时的压力。 包括CPU的异步绘制机制,来达到提交的位图本身就是一个层级非常少的视图,这样也可以减轻GPU的压力。
UIView的绘制原理
如果UIView实现了方法
方法: - (void)displayLayer:(CALayer*)layer;
就会进行异步绘制,反之,走系统绘制;
调用 UIView 的setNeedsDisplay之后并没有立即执行当前视图的绘制工作,而是在调用时立即调用当前view 的 layer 的同名方法,于是在当前 layer 上打上了一个脏标记,在当前 runloop快要结束的时候才会调用CALayer display方法,然后才会进行当前视图真正的绘制流程。
- 系统绘制流程图如下:
drawLayer: inContext: 实现了这个方法,就不会再去走 drawRect:,没实现就直接走drawRect:
屏蔽drawLayer: inContext: 则会进入drawRect:方法。这里为什么要有个drawLayer: inContext:方法呢?为什么不直接drawRect:,我猜想可能是为了增加灵活性吧,drawRect:是UIView的一个方法,只能在UIView中调用。而drawLayer:inContext:则更加自由,是要实现了CALayer的代理的类都可以使用drawLayer: inContext:。
允许我们在系统的绘制之上,做一些其他的绘制工作
-
异步绘制
图片.png
左侧是主队列,右侧是全局并发队列,假如我们在某一时机一个 View 调用了setNeedsDisplay这个方法之后呢,在当前 runloop 将要结束的时候呢,系统就会调用视图所对应 layer 的display方法。
如果我们的代理实现了displayLayer:这个函数的时候,会调用代理的displayLayer:这个方法,然后会通过子线程的切换,在子线程中进行位图的绘制。主线程这会就可以做一些其他的工作。
子线程在全局并发队列中所做的工作:
- 通过CGBitmapContextCreate()这个 Core Graphics 的函数来创建位图的一个上下文。
- 再通过 Core Graphics 的相关API做当前UI控件的一些绘制工作。
- 之后通过 Core Graphics 函数CGBitmapContextCreateImage()来根据当前绘制的上下文来生成一张 CGImage 的图片。
之后再回到主队列当中,提交这个位图,设置给 CALayer 的contents属性,这样就完成了一个UI控件 的异步绘制过程
一般遇到性能问题时,考虑以下问题:
是否受到CPU或者GPU的限制?
是否有不必要的CPU渲染?
是否有太多的离屏渲染操作?
是否有太多的图层混合操作?
是否有奇怪的图片格式或者尺寸?
是否涉及到昂贵的view或者效果?
view的层次结构是否合理?