阅读 558

iOS探索:UI视图之卡顿、掉帧及绘制原理

在开始理解卡顿、掉帧及绘制原理前,首先让我们先了解下图像的显示原理

图像显示原理

WX20181206-150708@2x.png

  • 关于CPU和GPU都是通过总线连接起来的,在CPU当中输出的往往是一个位图,再经由总线在合适的时机传递个GPU

  • GPU拿到这个位图之后,会对这个位图的图层进行渲染,包括纹理的合成等

  • 之后会把这个结果放到帧缓冲区中,然后视频控制器会按照VSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器,达到最终的显示效果

那么接下来让我们看一下CPU和GPU分别做了哪些事情

WX20181206-153514@2x.png

  • 首先当我们创建一个UIView控件的时候,其中负责显示的CALayer

  • CALayer中有一个contents属性,就是我们最终要绘制到屏幕上的一个位图,比如说我们创建了一个UILabel,那么在contents里面就放了一个关于Hello world的文字位图

  • 然后系统会在一个合适的时机回调给我们一个drawRect:的方法,这个方法中我们可以去绘制一些自定义的内容

  • 绘制好了之后,最终会由Core Animation这个框架提交给GPU部分的OpenGL渲染管线,进行最终的位图的渲染,包括纹理合成等,然后显示在屏幕上

那么CPU和GPU具体做了哪些工作承担呢

CPU

具体分为四个阶段

  • Layout:这里主要涉及到一些UI布局,文本计算等,例如一个label的size

  • Display:绘制阶段,例如drawRect方法就在这一步骤中

  • Prepare:图片的编解码等操作在此步骤中

  • Commit:提交位图

GPU渲染管线

  • 顶点着色

  • 图元装配

  • 光栅化

  • 片段着色

  • 片段处理

UI卡顿、掉帧的原因

WX20181206-160621@2x.png

在显示器中是固定的频率,比如iOS中是每秒60帧(60FPS),即每帧16.7ms

从上图中可以看出,每两个VSync信号之间有时间间隔(16.7ms),在这个时间内,CPU主线程计算布局,解码图片,创建视图,绘制文本,计算完成后将内容交给GPU,GPU变换,合成,渲染(详细可学习 OpenGL相关课程),放入帧缓冲区

假如16.7ms内,CPU和GPU没有来得及生产出一帧缓冲,那么这一帧会被丢弃,显示器就会保持不变,继续显示上一帧内容,这就将导致导致画面卡顿

所以无论CPU,GPU,哪个消耗时间过长,都会导致在16.7ms内无法生成一帧缓存

卡顿、掉帧优化方案切入点

  • CPU CPU在准备下一帧的所做的工作非常多导致耗时,基于减轻CPU工作时长和压力来达到一个优化效果 1、部分对象的创建、调整和销毁可以放到子线程去做 2、预排版( 布局计算、文本计算),这些计算也可以放到子线程去做,这样主线程也可以有更多的时间去响应用户的交互 3、预渲染(文本等异步绘制、图片编解码等)

  • GPU 1、纹理渲染:假如说我们触发了离屏渲染,例如我们设置圆角时对maskToBounds的设置,包括一些阴影、蒙层等都会触发GPU层面的离屏渲染,对于这种情况下,GPU对于纹理渲染的工作量就会非常的大,我们可以基于此对GPU进行优化,就是尽量减少离屏渲染,我们也可以通过CPU的异步绘制来减轻GPU的压力

    2、视图混合: 比如说我们视图层级比较复杂,视图之间层层叠加,那么GPU就要做每一个视图的合成,合成每一个像素点的像素值,如果我们可以减少视图的层级,也是可以减轻GPU的压力,我们也可以通过CPU的异步绘制机制来达到一个提交的位图本身就是一个层级比较少的位图

UIView的绘制原理

流程图

QQ20181206-211905@2x.png

  • 当我们调用[UIView setNeedsDisplay]这个方法时,其实并没有立即进行绘制工作,系统会立刻调用CALayer的同名方法,并且会在当前layer上打上一个标记,然后会在当前runloop将要结束的时候调用[CALayer display]这个方法,然后进入我们视图的真正绘制过程

  • 而在[CALayer display]这个方法的内部实现中会判断这个layer的delegate是否响应displayLayer:这个方法,如果不响应这个方法,就会进入到系统绘制流程中;如果响应这个方法,那么就会为我们提供异步绘制的入口

上面就是UIView的绘制原理,接下来我们看一下系统绘制流程是怎样的

老规矩,先上流程图

QQ20181206-213639@2x.png

  • 在CALayer内部会先创建backing store,我可以理解为CGContext,我们一般在drawRect:方法中通过上下文堆栈当中取出栈顶的context,也就是上下文

  • 然后这个layer会判断是否有代理,如果没有代理,那么就会调用[CALayer drawInCotext:];如果有代理,会调用代理的drawLayer:inContext:方法,然后做当前视图的绘制工作这一步是发生在系统内部的),然后在一个合适的时机给与我们这个十分熟悉的[UIView drawRect:]方法的回调,[UIView drawRect:]这个方法默认是什么都不做,,系统给我们开这个口子是为了让我们可以再做一些其他的绘制工作

  • 然后无论是哪个分支,最终都会由CALayer上传对应的backing store(可以理解为位图)给GPU,然后就结束了系统默认的绘制流程

那么问题来了,我们如何进行异步绘制呢

实际上我们就需要借用系统给开的这个口子,即[layer.delegate displayLayer:]

  • 在这个异步绘制过程中就需要代理负责生成对应的bitmap(位图)

  • 同时设置bitmap作为layer.contents属性的值

国际惯例,流程图走一波(原谅我画图能力实在有限TT)

QQ20181206-220620@2x.png

  • 假如说我们在某一个时机调用了[view setNeedsDisplay]这个方法,系统会在当前runloop将要结束的时候调用[CALyer display]方法,然后如果我们这个layer的代理实现了[view displayLayer]这个方法

  • 然后会通过子线程的切换,我们在子线程中去做一个位图的绘制,主线程可以去做一些其他的操作

  • 在子线程中第一步先通过CGBitmapContextCreate()方法来创建一个位图的上下文,然后我们通过CoreGraphic API可以做当前UI控件的一些绘制工作,最后我们再通过CGBitmapContextCreateImage()这个函数来根据当前所绘制的上下文来生成一张CGImage图片

  • 最后回到主线程来提交这个位图,设置layer的contents属性,这样就完成了一个UI控件的异步绘制过程

离屏渲染 (便于理解视图卡顿、掉帧中对GPU的开销)

离屏渲染指的是GPU在当前屏幕缓冲区以外开辟了一个缓冲区进行渲染操作

当前屏幕渲染不需要额外创建新的缓存,也不需要开启新的上下文,相对于离屏渲染性能更好。但是受当前屏幕渲染的局限因素限制(只有自身上下文、屏幕缓存有限等),当前屏幕渲染有些情况下的渲染解决不了的,就使用到离屏渲染

离屏渲染对性能的的代价是很高的,主要体现在:

  • 创建了新的缓冲区

  • 上下文的频繁切换

导致产生离屏渲染的原因:

  • shouldRasterize(光栅化)

  • masks(遮罩)

  • shadows(阴影)

  • edge antialiasing(抗锯齿)

  • group opacity(不透明)

  • 复杂形状设置圆角等

  • 渐变

关注下面的标签,发现更多相似文章
评论