阅读 5379

OpenGL-05-屏幕卡顿原因及iOS下的渲染

今天我们来看一下:

图片撕裂、掉帧、屏幕卡顿的原因、iOS下的渲染框架、CoreAnimation的渲染流水线、UIView与CALayer的区别及部分OpenGL相关知识补充。

一、图片撕裂、掉帧、屏幕卡顿

1、图片撕裂

撕裂:(图像显示过程是不断从帧缓冲区获取一帧一帧数据进行显示的)在渲染过程中,帧缓冲区中有旧数据在进行显示,在继续扫描读取的时候新的数据被处理好放入了缓冲区,这时候造成了上部分显示旧数据,下部分显示新数据。导致显示的图片出现错位、不匹配的情况

什么时候会出现撕裂? 当CPU和GPU的计算能力跟不上所需要的帧率(60FPS),此时会可能发生撕裂。一般是在低端设备上,加载一个高FPS的视频或者游戏场景。iOS设备不太常见,大多在安卓设备上出现。

  • 补充一下【屏幕成像的过程】:

需要显示的图片 ===> GPU进行图像渲染 (最后得到位图)===> 存入帧缓冲区 <===> 由视频控制器进行读取 ===> (经过数模转换,从左上角逐行扫描)显示在屏幕上

处理方法:垂直同步Vsync + 双缓存区 DeubleBuffering。这种方案苹果推出的,是强制要求同步,且是以掉帧为代价的。

  • 垂直同步: 在帧缓存区加锁,等这张图片扫描完才进行更新,防止出现撕裂
  • 双缓存区:因为CPU和GPU计算时间不同,存在时间差。从而在旧数据未读取完,新数据已经处理好加入了缓冲区,造成了撕裂。于是要想从根本上解决撕裂,就得用到双缓存区

也就是说,垂直同步:防止出现撕裂。双缓存区:从根本上解决撕裂。 这种方法治标不治本,标是做出了人眼看不出撕裂的操作,本是CPU和GPU比较老

这里在网上找到一张很形象的流程图:

2、掉帧

当我们启用了垂直同步+ 双缓存区的方案,解决了屏幕撕裂的问题的同时,也是会产生新的问题。

掉帧:简单来说就是,重复渲染同一帧数据。

(在我们接收到垂直同步的时候,由于CPU和GPU的速度问题,导致数据还没有准备好,这时时视频控制器拿不到frameBuffer)

如下图,当前屏幕显示的是A,在收到垂直信号后,CPUHE GPU还没有处理好B,这时候该显示B,但是显示的是A。重复显示了A就是掉帧。

处理方法:引入三缓冲区。(注意:这里并不是彻底解决了掉帧,只是比双缓冲方案比较,减少了掉帧情况)它主要是为了充分利用CPU和GPU的空闲时间,开辟ABC三个帧缓冲区,A显示屏幕,B也渲染好了,C再从GPU中拿取渲染数据,当屏幕缓冲区和帧缓冲区都弄好了,视频控制器再指向帧缓冲区的另外一个再显示,这样进行交替,就减少了掉帧的情况

3、屏幕卡顿

屏幕卡顿:也就是掉帧问题导致的。【这里也是一个高频面试题】

屏幕刷新频率必须要足够高才能流畅。对于 iPhone 手机来说,屏幕最大的刷新频率是 60 FPS,一般只要保证 50 FPS 就已经是较好的体验了。但是如果掉帧过多,导致刷新频率过低,就会造成不流畅的使用体验。

主要3个原因:

  • CPU和GPU在 渲染流水线(下文中介绍)中耗时过长,导致下一帧数据没准备好,获取的还是上一帧数据,产生掉帧现象
  • 在解决屏幕撕裂问题上,我们使用垂直同步+ 双缓存区方案,但这是以掉帧为代价进行的
  • 虽然我们用了三缓冲区,也只是减少了掉帧的情况。并不是不会出现掉帧

二、iOS下的渲染

1、渲染框架

2、渲染流程

如图: 1、我们的App通过调用CoreGraphics、CoreAnimation、CoreImage等框架的接口触发图形渲染操作 2、CoreGraphics、CoreAnimation、CoreImage等框架将渲染交由OpenGL ES/Metal来驱动GPU进行渲染,最终显示在屏幕上。(上文中也说了OpenGL ES 是跨平台的,在iOS中,APP调用CoreAnimation提供窗口 来使用OpenGL ES)

3、CoreAnimation

苹果的官方描述: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中的渲染流水线

如图,整个流程分了两部分: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、着色器渲染流程

  • 渲染流程中,必须存储2种着色器,分别是:顶点着色器、片元着色器
  • 顶点着色器拿到顶点数据进行几何操作 ==> 通过图元装配连接 ==> 剪切、光栅化 ==> 片元着色器进行每一个像素点的着色等操作 ==>显示

2、CPU与GPU

CPU:计算机的运算核心、控制核心

  • 处理复杂的逻辑、数据
  • 依赖性高。利用时间片的切换达到并发效果
  • CPU上拥有控制单元、计算单元、缓存单元等
  • CPU做图片解码

GPU:负责绘图运算的微处理器

  • GLSL语言及其简单,处理单一的运算, 利用并行处理能力解决运算任务
  • 依赖性低。拥有很多计算单元,很好的达到高并发
  • GPU上有大量的计算单元
  • GPU做视频解码

图片是怎么显示的? 1、CPU做图片解码转换成位图 2、GPU纹理混合,经过着色器渲染流程,把数据放到帧缓冲区 3、等待时钟信号(垂直/水平 同步信号) 4、渲染上屏

图片的强行解压,就是对图片进行重新绘制,得到新的位图。(iOS需要使用的CGBitmapContextCreate,可以看下YYImage和SDWebImage看看是怎么写的)

3、计算机显示方式

最初形态:随机扫描显示,如图

后来演变成了:光栅扫描显示,如图

光栅扫描需要注意的是:因为图像是由像素阵列组成的,显示一张图像的时间与图像本身的复杂度无关。(显示过程中,是在不断的刷新,人眼1秒16帧以上是看不出来的。)

光栅扫描显示系统的组成:

  • 显示器:显示的内容来自帧缓冲区
  • 视频控制器:负责控制刷新的部件进行刷新。读帧缓冲区 —进行显示绘制 —显示在显示器上
  • 帧缓冲区:每一个像素点,存储了颜色值、帧缓存(显存:存储显卡处理过或者即将提取的渲染数据)

一个 60 x 60 的位图所占的位置有多大? 大小就是3600 x 4 = 14400,那么就需要这么大的空间存储这张位图

文章分类
iOS
文章标签