OpenGL-05-屏幕卡顿原因及iOS下的渲染
今天我们来看一下:
图片撕裂、掉帧、屏幕卡顿的原因、iOS下的渲染框架、CoreAnimation的渲染流水线、UIView与CALayer的区别及部分OpenGL相关知识补充。
一、图片撕裂、掉帧、屏幕卡顿
1、图片撕裂
image.png撕裂:(图像显示过程是不断从帧缓冲区获取一帧一帧数据进行显示的)在渲染过程中,帧缓冲区中有旧数据在进行显示,在继续扫描读取的时候新的数据被处理好放入了缓冲区,这时候造成了上部分显示旧数据,下部分显示新数据。导致显示的图片出现错位、不匹配的情况
什么时候会出现撕裂?
当CPU和GPU的计算能力跟不上所需要的帧率(60FPS),此时会可能发生撕裂。一般是在低端设备上,加载一个高FPS的视频或者游戏场景。iOS设备不太常见,大多在安卓设备上出现。
- 补充一下【屏幕成像的过程】:
需要显示的图片 ===> GPU进行图像渲染 (最后得到位图)===> 存入帧缓冲区 <===> 由视频控制器进行读取 ===> (经过数模转换,从左上角逐行扫描)显示在屏幕上
处理方法:垂直同步Vsync + 双缓存区 DeubleBuffering。这种方案苹果推出的,是强制要求同步,且是以掉帧为代价的。
- 垂直同步: 在帧缓存区加锁,等这张图片扫描完才进行更新,防止出现撕裂
- 双缓存区:因为CPU和GPU计算时间不同,存在时间差。从而在旧数据未读取完,新数据已经处理好加入了缓冲区,造成了撕裂。于是要想从根本上解决撕裂,就得用到双缓存区
也就是说,垂直同步:防止出现撕裂。双缓存区:从根本上解决撕裂。
这种方法治标不治本,标是做出了人眼看不出撕裂的操作,本是CPU和GPU比较老
这里在网上找到一张很形象的流程图:
image.png
2、掉帧
当我们启用了垂直同步+ 双缓存区的方案,解决了屏幕撕裂的问题的同时,也是会产生新的问题。
掉帧:简单来说就是,重复渲染同一帧数据。
(在我们接收到垂直同步的时候,由于CPU和GPU的速度问题,导致数据还没有准备好,这时时视频控制器拿不到frameBuffer)
如下图,当前屏幕显示的是A,在收到垂直信号后,CPUHE GPU还没有处理好B,这时候该显示B,但是显示的是A。重复显示了A就是掉帧。
image.png
image.png处理方法:引入三缓冲区。(注意:这里并不是彻底解决了掉帧,只是比双缓冲方案比较,减少了掉帧情况)它主要是为了充分利用CPU和GPU的空闲时间,开辟ABC三个帧缓冲区,A显示屏幕,B也渲染好了,C再从GPU中拿取渲染数据,当屏幕缓冲区和帧缓冲区都弄好了,视频控制器再指向帧缓冲区的另外一个再显示,这样进行交替,就减少了掉帧的情况
3、屏幕卡顿
屏幕卡顿:也就是掉帧问题导致的。【这里也是一个高频面试题】
屏幕刷新频率必须要足够高才能流畅。对于 iPhone 手机来说,屏幕最大的刷新频率是 60 FPS,一般只要保证 50 FPS 就已经是较好的体验了。但是如果掉帧过多,导致刷新频率过低,就会造成不流畅的使用体验。
主要3个原因:
- CPU和GPU在 渲染流水线(下文中介绍)中耗时过长,导致下一帧数据没准备好,获取的还是上一帧数据,产生掉帧现象
- 在解决屏幕撕裂问题上,我们使用垂直同步+ 双缓存区方案,但这是以掉帧为代价进行的
- 虽然我们用了三缓冲区,也只是减少了掉帧的情况。并不是不会出现掉帧
二、iOS下的渲染
1、渲染框架
image.png2、渲染流程
image.png如图:
1、我们的App通过调用CoreGraphics、CoreAnimation、CoreImage等框架的接口触发图形渲染操作
2、CoreGraphics、CoreAnimation、CoreImage等框架将渲染交由OpenGL ES/Metal来驱动GPU进行渲染,最终显示在屏幕上。(上文中也说了OpenGL ES 是跨平台的,在iOS中,APP调用CoreAnimation提供窗口 来使用OpenGL ES)
3、CoreAnimation
image.png苹果的官方描述:Render, compose, and animate visual elements.
其实,CoreAnimation本质上可以理解为一个复合引擎。渲染、构建和实现动画。
- 基于CoreAnimation构建的框架有两个:UIKit(iOS)和APPKit(Mac OSX)
- CoreAnimation 是基于Metal 、CoreGraphics封装的
拓展:【UIKit是iOS平台的渲染框架,APPKit是Mac OSX系统下的渲染框架。由于iOS和Mac两个系统的界面布局并不是一致的,iOS是基于多点触控的交互方式,而Mac OSX是基于鼠标键盘的交互方式,且分别在对应的框架中做了布局的操作,所以并不需要layer载体去布局,且不用迎合任何布局方式。】
苹果基于UIView和CALayer提供两个平行的层级关系(UIKit 和APPKit):
- 职责分离,可以避免大量重复代码
- 两个系统交互规则不一样(手机是通过触发点击事件,电脑是通过鼠标键盘等输入),它们虽然功能上相似,但实现上有显著的区别
4、CoreAnimation中的渲染流水线
image.png如图,整个流程分了两部分:CoreAnimation部分、GPU部分
在CoreAnimation部分下的3步操作:
- HandleEvents 事件处理
- Commit Transaction 提交图片
- Render Server 交给CPU解码
CoreAnimation把解码好的东西 ===> 提交给OpenGL ===> 调度GPU ===> 进行渲染流程 [下文第三部分中会给出详解:顶点数据--->顶点着色器--->片元着色器]===> 等待下一个runloop去显示
Commit Transaction中间发生了什么
主要进行的是:Layout、Display、Prepare、Commit 等四个具体的操作。
- Layout(构建视图):这个阶段是在 CPU 中进行,调用重载的 layoutSubviews 方法、创建视图并通过 addSubview 方法添加子视图、计算视图布局(即所有的 Layout Constraint)
- Display(绘制视图):这个阶段主要是交给 Core Graphics 进行视图的绘制,根据上一阶段 Layout 的结果得到图元信息。【如果重写了 drawRect: 方法,这个阶段会直接调用 Core Graphics 绘制方法得到 bitmap 数据,同时系统会额外申请一块内存,用于暂存绘制好的 bitmap。会有额外的开销,使用CPU和内存】
- Prepare(准备工作):图片解码和转换
- Commit(提交):图层打包并发送到 Render Server
Render Server操作分析:
在GPU部分下的操作:
- GPU中通过顶点着色器、片元着色器完成对显示内容的渲染,将结果存入帧缓存区
- GPU通过帧缓存区、视频控制器等相关部件,将其显示到屏幕上
如上整个流水线是连贯的两部分。
5、UIView与CALayer
- UIView基于UIKit框架,可以处理用户触摸事件,并管理子视图
- CALayer基于CoreAnimation,而CoreAnimation是基于QuartzCode的。所以CALayer只负责显示,不能处理用户的触摸事件
- 从父类来说,CALayer继承的是NSObject,而UIView是直接继承自UIResponder的,所以UIView相比CALayer而言,只是多了事件处理功能
- 从底层来说,UIView属于UIKit的组件,而UIKit的组件到最后都会被分解成layer,存储到图层树中
- 在应用层面来说,需要与用户交互时,使用UIView,不需要交互时,使用两者都可以
- 总结一下区别:
UIView:
1、负责绘制图形和动画操作
2、布局及子view的管理
3、处理点击事件
4、属于UIKit,继承自 UIResponder
CALayer:
1、只做渲染和动画功能
2、显示的是位图(bitmap)
3、属于CoreAnimation(不仅仅用于UIKit,也用于APPKit),继承自 NSObject
- 总结一下核心关系:
1、CALayer 是 UIView 的属性之一,负责渲染和动画,提供可视内容的呈现。
2、UIView 提供了对 CALayer 部分功能的封装,同时也另外负责了交互事件的处理。
iOS下界面触发渲染的流程:
有两种触发方式:
1、通过loadView中子View的drawRect方法触发:会回调CoreAnimation中监听Runloop的BeforeWaiting的【RunloopObserver】,通过RunloopObserver来进一步调用CoreAnimation内部的【CA::Transaction::commit()】,进而一步步走到【drawRect】方法
2、用户点击事件触发:唤醒Runloop',由【source1】处理(__IOHIDEventSystemClientQueueCallback),并且在下一个runloop里由【source0】转发给UIApplication(_UIApplicationHandleEventQueue),从而能通过source0里的事件队列来调用CoreAnimation内部的【CA::Transaction::commit()】方法,进而一步一步的调用【drawRect】。已经到了CoreAnimation的内部,即调用CA::Transaction::commit();来创建CATrasaction,然后进一步调用 CALayer drawInContext:()
在drawRect:方法里可以通过CoreGraphics函数或UIKit中对CoreGraphics封装的方法进行画图操作
将绘制好的位图交由CALayer,由OpenGL ES 传送到GPU的帧缓冲区
等屏幕接收到垂直信号后,就读取帧缓冲区的数据,显示到屏幕上
三、OpenGL知识补充
1、着色器渲染流程
image.png- 渲染流程中,必须存储2种着色器,分别是:顶点着色器、片元着色器
- 顶点着色器拿到顶点数据进行几何操作 ==> 通过图元装配连接 ==> 剪切、光栅化 ==> 片元着色器进行每一个像素点的着色等操作 ==>显示
2、CPU与GPU
CPU:计算机的运算核心、控制核心
- 处理复杂的逻辑、数据
- 依赖性高。利用时间片的切换达到并发效果
- CPU上拥有控制单元、计算单元、缓存单元等
- CPU做图片解码
GPU:负责绘图运算的微处理器
- GLSL语言及其简单,处理单一的运算, 利用并行处理能力解决运算任务
- 依赖性低。拥有很多计算单元,很好的达到高并发
- GPU上有大量的计算单元
- GPU做视频解码
图片是怎么显示的?
1、CPU做图片解码转换成位图
2、GPU纹理混合,经过着色器渲染流程,把数据放到帧缓冲区
3、等待时钟信号(垂直/水平 同步信号)
4、渲染上屏
图片的强行解压,就是对图片进行重新绘制,得到新的位图。(iOS需要使用的CGBitmapContextCreate,可以看下YYImage和SDWebImage看看是怎么写的)
3、计算机显示方式
最初形态:随机扫描显示,如图
image.png
后来演变成了:光栅扫描显示,如图
image.png
光栅扫描需要注意的是:因为图像是由像素阵列组成的,显示一张图像的时间与图像本身的复杂度无关。(显示过程中,是在不断的刷新,人眼1秒16帧以上是看不出来的。)
光栅扫描显示系统的组成:
- 显示器:显示的内容来自帧缓冲区
- 视频控制器:负责控制刷新的部件进行刷新。读帧缓冲区 —进行显示绘制 —显示在显示器上
- 帧缓冲区:每一个像素点,存储了颜色值、帧缓存(显存:存储显卡处理过或者即将提取的渲染数据)
一个60*60的位图所占的位置有多大?
大小就是3600 * 4=14400,那么就需要这么大的空间存储这张位图