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

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

图像显示原理

WX20181206-150708@2x.png

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

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

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

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

WX20181206-153514@2x.png

  • 首先当咱们建立一个UIView控件的时候,其中负责显示的CALayeroop

  • 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工做时长和压力来达到一个优化效果 一、部分对象的建立、调整和销毁能够放到子线程去作 二、预排版( 布局计算、文本计算),这些计算也能够放到子线程去作,这样主线程也能够有更多的时间去响应用户的交互 三、预渲染(文本等异步绘制、图片编解码等)

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

    二、视图混合: 好比说咱们视图层级比较复杂,视图之间层层叠加,那么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(不透明)

  • 复杂形状设置圆角等

  • 渐变

相关文章
相关标签/搜索