iOS中的绘制和渲染

iOS的渲染和绘制机制

显示器原理和技术

电子枪逐行扫描(HSync),一帧画面绘制完成后,复原准备下一帧(VSync信号),此信号产生的频率即刷新率帧率。当缓冲区只有一个时,视频帧的读取和刷新效率会很低(GPU渲染一帧就要等待视频控制器读取一帧去显示),期间GPU输出的画面会丢失,即会造成画面闪烁,于是引入双缓冲区的技术。

双缓冲区:GPU预先渲染好一帧放入正常的缓冲区,下一帧渲染好之后直接把视频控制器的指针指向第二个缓冲区(back换到front上),利用了更多显存和CUP。而且当前一帧未读取完成下一帧就换上来了就会造成上下半段画面割裂。于是引入GPU的V-Sync垂直同步技术。

V-Sync:垂直同步,等待V-Sync信号发出后才进行新一帧的渲染和缓冲区更新,让GPU增加每一帧的发送间隔保持和屏幕的刷新率相同,确保显示器有时间处理视频帧,避免画面割裂,但需要更多计算资源,带来部分延迟。

注1:V-Sync由系统硬件时钟驱动,在两个V-Sync信号之间,屏幕会扫描Touch,并包含在下一个V-Sync信号里,runloop接收时进行处理。所以动画最多60帧,触摸最多每秒捕捉60个点。

注2:V-Sync初衷是为了控制GPU渲染时间,解决GPU输出的不稳定导致和屏幕显示撕裂的问题。但后来由于周期稳定、与屏幕扫描周期重叠,于是在屏幕的触控捕捉和动画的实现流程中也起到了关键作用,于是形成了一个V-Sync体系,渲染和显示都是踩着V-Sync的点进行的。


从渲染到显示(UI刷新原理)

V-Sync通过Mach port唤醒了app主线程RunLoop

→代码更改视图的操作被CALayer捕获并通过CATransaction标记为待处理的中间状态

→操作结束后,RunLoop进入休眠前的回调,触发了CoreAnimation的监听(进入Core Animation Pipeline 管线)

→CA对做了标记的中间状态的Layer进行计算和属性合并(如果有动画则通过DisplayLink等机制多次触发)(CPU),Commit Transation打包layer

→提交到Render Server进程生成图层树(通过mach port进程间通信)(CPU)

→然后提交GPU进行渲染,并输出到缓冲区

→视频控制器从缓冲区逐行读取帧数据,并传递给显示器显示


Core Animation Pipeline流水线

框架:UIKit负责响应用户操作(不具备渲染能力);CoreAnimation负责所有绘制、显示、动画效果;OpenGL ES提供2D/3D渲染服务;Core Graphics提供2D渲染服务;Graphics Hardware即GPU。

具体流程(在此之前需要计算的Layer已被标记)

1、Commit Transaction打包计算并提交视图变换:RunLoop进入休眠前调用layoutSubviews计算Layout布局;Display即重载drawRect绘制;Prepare图像解码和格式转换;Commit,CoreAnimation递归打包Layer,批量提交给Render Server(即 OpenGL ES & Core Graphics)。

2、Render Server构建图层树:解析上一步提交的信息并反序列化成图层树,随后根据layer的各种属性生成绘制指令,提交到GPU。

3、GPU渲染:GPU进行变换合成和渲染,3D转2D和光栅处理,并依次输出到缓冲区。

4、Display显示:从缓冲区取出渲染数据逐行扫描输入到屏幕上,如果没有更改,继续使用GPU中的缓存。

注1:改变Frame,更新UIView/Layer的层级,手动调用setNeedsLayout(对应标记layout阶段的计算)/setNeedsDisplay(对应Display阶段的绘制)后,这个视图就会被标记为待处理。

注2:如果主线程任务过多就会让layer的更新延迟造成卡顿。

注3:CoreAnimation计算期间会调用一次所有被标记的layer的layoutSubviews和drawRect方法。

注4:1步其实是CA在app内的视图计算打包过程(layout对应layoutSubviews的重载和addSubviews;Display对应drawRect使用CPU和内存绘制bitmap;Commit递归打包图层树,注意树的复杂度;CATransaction图层树分配到渲染树的机制,0.25秒的隐式动画)

注5:drawRect方法在绘制任何东西前都会产生巨大的性能开销——上下文切换。


UI卡顿

根本原因:1、2步图层树的准备和提交(计算)由CPU负责,3步的渲染由GPU负责,卡顿的原因就是这3步(+runloop中回调前的的主线程任务)没有在1/60 * 2秒(因为双缓存机制可以有1/60的冗余)内完成造成丢帧。


绘制卡顿分析之CPU资源消耗

1、视图的创建:会分配内存调整属性甚至有IO操作;尽量用轻量级的对象(CALayer比UIView轻量);不涉及UI操作尽量后台创建;不要用StoryBoard

2、视图的调整:CALayer内部没有属性(UIView也是CALayer映射过来的),是通过运行时动态方法解析resolveInstanceMethod为Layer临时创建方法,把属性保存到一个内部的字典中,同时还会回调delegate和创建动画,非常消耗资源,所以应避免调整视图层次、添加和移除视图等。

3、文本渲染:底层都是通过CoreText排版绘制为Bitmap,且都是在主线程进行,UILabel会进行多次计算,大量文本会增加CPU压力。解决:自定义控件,用底层的TextKit或CoreText进行异步绘制,能直接获取宽高信息,并缓存以备多次渲染(YYText、DTCoreText等)。

4、图片解码:普通的图片创建方法并不会立刻解码,而是设置到View或Layer中后、并被提交到GPU前CGImage才会在主线程解码,不可避免。常见的做法是后台线程把图片绘制到CGBitmapContext CreateImage直接创建图片CGImageRef。注:SDWebImage就有提前解码图片的逻辑(decodedImageWithImage方法),但imageNamed是读Assets的,马上解码加到内存里缓存起来。

5、drawRect方法:-drawLayer:inContext:的包装,CALayerDelegate的回调,视图被标记为待处理时,CA计算时会调用它,所以在此方法中使用CG绘图,会创建并切换绘制上下文,大小相当于一张满像素的全屏图片,造成巨大的开销。

6、其他:布局计算(frame/bounds/center,算高的缓存)、Autolayout(复杂布局有性能问题指数级上升)、图片绘制(画布创建图片并显示,CG线程安全可以放入异步线程)。

注:CGBitmapContextCreate画出来CGImageRef(bitmap位图),可以提前解码,直接赋值给layer的contents,提交绘制;区分UIGraphicsBeginImageContext,画出UIImage,比较常用,layer.render(in:context)可以把当前View截图返回UIImage。


绘制卡顿分析之GPU资源消耗

1、纹理的渲染:所有Bitmap(图片文本栅格化内容)最终都要由内存提交到显存,GPU调整和渲染Texture,如tableView中短时间显示大量图片(图片过大需要CPU预处理,纹理尺寸有上限)。

2、视图的混合:多个CALayer重叠时,GPU的混合过程也会消耗很多资源。应尽量减少视图数量和层次,不透明视图标明opaque = true可以只进行简单的纹理拷贝,避免无用的alpha通道合成。(Instruments中的color blended layers可以查验)

3、图形的生成:border,圆角,阴影,遮罩,CAShapeLayer矢量图形,通常会触发离屏渲染。


离屏渲染

定义:由于一些限制,先把渲染结果暂存在另外的区域(Offscreen Buffer),之后再写入FrameBuffer一般意义上的离屏渲染发生在GPU。但其实,UIView中实现的drawRect方法时,系统也会为这个View申请一块内存区域等待CoreGraphics可能的CGContext绘画操作,进一步说在iOS渲染流程的第一步中的光栅化操作(CoreAnimation把打包的Layer提交到RenderServer前的过程中的文字渲染和图片解码),都属于CPU渲染,即伪离屏渲染。

根本原因:通常Render Server遵循画家原则层层按序输出到FrameBuffer,但无法回头擦除/改变某部分,即对于每一层layer要么通过单次遍历就完成渲染,要么新开内存进行更复杂的中转修改、裁剪操作(离屏渲染)。

性能影响:最大的损耗在于上下文切换(保存当前的屏幕渲染环境,切换到屏幕外缓冲区,申请资源初始化环境开始一个新的绘制,完成后销毁这个环境,转换到帧缓冲区,每一帧都发生)。

常见场景:圆角(cornerRadius+clipsToBounds),阴影(阴影在layer内容的下方,但是阴影的本体都还没有画出),group opacity(layer树画完后统一加alpha),mask遮罩(应用在layer树之上,原理同opacity),UIBlurEffect(毛玻璃效果)

圆角方案:1、后台线程CoreGraphics裁剪图片;2、用和背景颜色相同的弧形框图片盖住四个角(视频的圆角)。

注1:cornerRadius不一定会触发离屏渲染,如给UILabel的layer设置backgroundColor+cornerRadius并不会出发离屏渲染。

注2:iOS9后,UIImageView中PNG图片设置圆角不会再触发离屏渲染了(如列表中的头像);但UIButton设置圆角依然会触发。


离屏渲染解决方案

1、利用CPU分担GPU离屏渲染的负担:如使用CoreGraphics给图片加入圆角,在CPU内完成,自行控制裁剪和缓存的时机。注:一般在后台线程完成;CPU只适合渲染静态元素;及时释放。

2、shouldRasterize:光栅化,会强行进行一次离屏渲染并缓存渲染结果,下一帧可以复用,有100ms缓存时间,但不适用动态的layer内容。


性能监测

1、CADisplayLink:和屏幕刷新率一致的定时器(内部实际是操作了一个Source),添加到RunLoop后每帧都会回调,如果两次刷新之间有耗时任务,则会造成页面丢帧卡顿。(FPSLabel封装了一下)。

2、Instuments - Core Animation:帧率FPS,离屏渲染offscreen passes,视图混合blending,图片转换。

3、Xcode - View Debugger:视图和特效开销,视图层次结构。

4、Instuments - Time Profiler:CPU和GPU开销,CPU渲染。


AsyncDisplayKit(改名Texture)

Facebook保持界面流畅性的框架

原理:通过把排版和绘制等耗时的UI操作尽量放到后台执行,不能推迟的比如视图的创建和属性调整等尽量推迟。ASDisplayNode对象,内部封装了UIView/Layer,具有相似的UI属性并可以后台更改,然后利用RunLoop在某个时机同步到主线程的UI对象上去。

机制:仿照QuartzCore/UIKit框架,在主线程的RunLoop中添加Observer监听BeforeWaiting和Exit,在回调里遍历待处理的任务。


主线程刷UI      

1、UIKit线程不安全,UI操作涉及到渲染和访问View的各种属性,异步操作下读写会有问题,加锁则会耗费大量资源降低运行速度,而且把UIKit设计成线程安全会存在先后逻辑的问题。(一个线程remove,一个线程click是否响应,在哪条线程响应)

2、UIApplication是在主线程初始化的,它负责了整个app的事件传递和响应,所以响应者(UIView)只有在主线程上才能对响应事件(Main RunLoop驱动着UI的刷新,在每次循环结束时进行一次绘制,如果后台操作线程不同步,则一个RunLoop无法处理完,会产生事件和显示不同步的问题)

3、UI的渲染需要在60帧的屏幕上同时更新,非主线程异步无法保证渲染过程能同步更新。


相关问题

GPU刷新率超过60HZ会怎样,怎么解决

双缓冲区和V-Sync垂直同步技术。屏幕来不及扫描,造成丢帧,画面闪烁,于是引入V-Sync等待屏幕发出信号后,GPU才开始渲染下一帧。


V-Sync与屏幕的扫描(触摸的捕捉)

本来是通过屏幕硬件扫描频率,来控制GPU刷新率,后来由于周期稳定,形成体系,与RunLoop触发周期、触摸点的捕捉直接相关。


描述一下UI刷新的原理,并分别指出CPU和GPU负责的步骤

Core Animation Pipeline(计算和渲染)的具体流程:V-Sync信号通过machport触发主线程RunLoop中的Source回调,代码中标记了待刷新的视图,会在BeforeWaiting回调中启动管线。CA处理计算后提交layer集合到RenderServer即OpenGL反序列化图层树,(之前都是CPU,涉及到CA和RenderServer两个框架),提交GPU渲染,输出到缓冲区由屏幕读取。管线中提交到RenderServer前有4个阶段,layout:调layoutSubviews等重载布局;display:调drawRect等重载绘制;preload:解码图片;commit:CATransation提交事务打包图层树,并提交到OpenGL。


UI卡顿的根本原因是什么,CPU和GPU分别有哪些工作资源消耗较大,如何解决

主线程任务中有耗时操作,CPU的计算(layout布局复杂、drawRect绘制耗时、preload图片过大等)和GPU的渲染(离屏渲染)工作没有及时完成

CPU:视图的创建和调整-使用轻量级Layer,避免复杂布局,减少调整;文本的渲染-UILabel会多次渲染,使用TextKit/CoreText并缓存;drawRect-创建并切换上下文造成巨大开销,尽量减少使用;图片解码-建议子线程重绘成位图强制提前解码(SDWebImage)。

GPU:纹理的渲染,栅格化-尽量减小纹理大小;图层的混合-减少层级,标记不透明避免无用的通道混合;圆角,阴影-注意添加方式,避免离屏渲染。


为什么一定要在主线程刷UI

UIKit线程不安全;只有主线程的RunLoop才能监听到事件源并响应;屏幕的60帧刷新是同步的,异步渲染不能保证刷新的同步性。


如何保持界面流畅(Texture原理,原AsyncDisplayKit)

内部封装了UIView/CALayer映射了属性,更改时异步调整,并在适当的时机通过RunLoop同步到主线程,即仿UIKit-监听BeforeWaiting回调时处理标记的视图。


什么是离屏渲染,离屏渲染的根本原因是什么,为什么会影响性能

由于画家原则的限制,先把渲染结果暂存在另外的buffer区域,再写入。一般意义发生在GPU,但drawRect中系统也会申请一块内存区域做绘制操作,或CoreAnimation打包layer的文本的渲染和图片的解码都是在其他内存区域渲染绘制,即伪离屏渲染。

根本原因:Render Server会顺序输出无法擦除,所以对圆角阴影等复杂的绘制需要新开内存进行中转裁剪等操作。

性能影响:最大损耗在于切换上下文,保存当前渲染环境,并申请资源初始化一个新的渲染环境,完成后销毁新的环境转换到缓冲区,每一帧都发生。


离屏渲染的常见场景和解决方案

常见的如圆角、阴影和mask遮罩等,主要通过CPU渲染分担计算来解决,如后台通过CoreGraphics裁剪出圆角;还可以设置shouldRasterize光栅化,强制CPU高速离屏渲染一次但缓存起来。


layoutSubviews和drawRect是何时调用的,调用drawRect有什么问题

主线程RunLoop的BeforeWaiting回调中,会调用标记待处理的视图的重载方法,drawRect一般是进行绘制需要切上下文和开绘制区域,如果视图更改频繁每帧都绘则CPU性能爆炸。


如何对监测UI性能

CADisplayLink:和屏幕刷新率一致,可以直观的反映FPS;Instrument的CA和Timer工具;Xcode的View Debugger工具。

你可能感兴趣的:(iOS中的绘制和渲染)