030*:界面优化(CoreImage、CoreGraphics、OpenGL ES、metal、CoreAnimation【CALayer】)(预排版,异步渲染,按需加载,动态添加控件,避免使用透明度和圆角,离屏渲染,异步渲染合成一张图片)
问题
(CoreImage、CoreGraphics、OpenGL ES、metal、CoreAnimation【CALayer】)
1:
目录
1:卡顿的原理
2:卡顿监控
3:卡顿的优化
预备
正文
一:卡顿的原理
1:界面卡顿
通常来说,计算机中的显示过程是下面这样的,通过CPU
、GPU
、显示器
协同工作来将图片显示到屏幕上
图像显示过程
最开始时,FrameBuffer只有一个,这种情况下FrameBuffer的读取和刷新有很大的效率问题,为了解决这个问题,引入了双缓存区
。即双缓冲机制
。在这种情况下,GPU
会预先渲染好一帧放入FrameBuffer
,让视频控制器读取,当下一帧渲染好后,GPU会直接将视频控制器的指针指向第二个FrameBuffer
。
双缓存机制虽然解决了效率问题,但是随之而言的是新的问题,当视频控制器还未读取完成时,例如屏幕内容刚显示一半,GPU将新的一帧内容提交到FrameBuffer,并将两个FrameBuffer而进行交换后,视频控制器就会将新的一帧数据的下半段显示到屏幕上,造成屏幕撕裂
现象
为了解决这个问题,采用了垂直同步信号机制
。当开启垂直同步后,GPU会等待显示器的VSync信号发出后,才进行新的一帧渲染和FrameBuffer更新。而目前iOS设备中采用的正是双缓存区+VSync
主要有以下三种原因
- CPU和GPU在渲染的流水线中耗时过长,导致从缓存区获取位图显示时,下一帧的数据还没有准备好,获取的仍是上一帧的数据,产生掉帧现象,掉帧就会导致屏幕卡顿
- 苹果官方针对屏幕撕裂问题,目前一直使用的方案是
垂直同步+双缓存区
,可以从根本上防止和解决屏幕撕裂,但是同时也导致了新的问题掉帧
。虽然我们采用了双缓存区,但是我们并不能解决CPU和GPU处理图形图像的速度问题,导致屏幕在接收到垂直信号时,数据尚未准备好,缓存区仍是上一帧的数据,因此导致掉帧 - 在垂直同步+双缓存区的方案上,再次进行优化,将双缓存区,改为
三缓存区
,这样其实也并不能从根本上解决掉帧的问题,只是比双缓存区掉帧的概率小了很多,仍有掉帧的可能性,对于用户而言,可能是无感知的。
1:屏幕撕裂就类似于这样的情形
- 将需要显示的图像,经由GPU渲染
- 将渲染后的结果,存储到帧缓存区,帧缓存区中存储的格式是位图
- 由视屏控制器从帧缓存区中读取位图,交由显示器,从左上角逐行扫描进行显示
屏幕撕裂的原因
- 在屏幕显示图形图像的过程中,是不断从帧缓存区获取一帧一帧数据进行显示的,
- 然后在渲染的过程中,帧缓存区中仍是旧的数据,屏幕拿到旧的数据去进行显示,
-
在旧的数据没有读取完时 ,新的一帧数据处理好了,放入了缓存区,这时就会导致屏幕另一部分的显示是获取的新数据,从而导致屏幕上呈现图片不匹配,人物、景象等错位显示的情况。
图示如下:
2:垂直同步+双缓存,苹果官方的解决方案
苹果官方针对屏幕撕裂现象,目前一直采用的是 垂直同步+双缓存
,该方案是强制要求同步,且是以掉帧为代价的。
以下是垂直同步+双缓存的一个图解过程,
显示器在显示完一帧之后,发送垂直同步信号到CPU、GPU,如果这时候CPU和GPU在信号时间内没有完成计算或渲染内容,也就是缓存区没有内容可取,此时显示器还是会显示上一帧的内容,这时手机界面就会给人以卡顿的感觉。这也就是卡顿的主要原因了。
此时如果要对界面优化,主要就是针对CPU和GPU资源消耗进行优化了。
2:iOS中的渲染
2.1:在iOS中渲染的整体流程如下所示
- App通过调用CoreGraphics、CoreAnimation、CoreImage等框架的接口触发图形渲染操作
- CoreGraphics、CoreAnimation、CoreImage等框架将渲染交由OpenGL ES,由OpenGL ES来驱动GPU做渲染,最后显示到屏幕上
- 由于OpenGL ES 是跨平台的,所以在他的实现中,是不能有任何窗口相关的代码,而是让各自的平台为OpenGL ES提供载体。在ios中,如果需要使用OpenGL ES,就是通过CoreAnimation提供窗口,让App可以去调用。
CPU:coreimage
GPU:CoreGraphics
metal
CoreAnimation是metal基础上封装。
iOS中渲染框架总结
主要由以下六种框架,表格中已经说明了,就不再详细解释了
2.2:View 与 CALayer 的关系
1:首先分别简单说下UIView和CALayer各自的作用
UIView
- UIView属于UIKIt
- 负责绘制图形和动画操作
- 用于界面布局和子视图的管理
- 处理用户的点击事件
CALayer
- CALayer属于CoreAnimation
- 只负责显示,且显示的是位图
- CALayer既用于UIKit,也用于APPKit,
==> UIKit是iOS平台的渲染框架,APPKit是Mac OSX系统下的渲染框架,
==> 由于iOS和Mac两个系统的界面布局并不是一致的,iOS是基于多点触控的交互方式,而Mac OSX是基于鼠标键盘的交互方式,且分别在对应的框架中做了布局的操作,所以并不需要layer载体去布局,且不用迎合任何布局方式。
【面试题】UIView和CALayer的关系
- UIView基于UIKit框架,可以处理用户触摸事件,并管理子视图
- CALayer基于CoreAnimation,而CoreAnimation是基于QuartzCode的。所以CALayer只负责显示,不能处理用户的触摸事件
- 从父类来说,CALayer继承的是NSObject,而UIView是直接继承自UIResponder的,所以UIVIew相比CALayer而言,只是多了事件处理功能,
- 从底层来说,UIView属于UIKit的组件,而UIKit的组件到最后都会被分解成layer,存储到图层树中
- 在应用层面来说,需要与用户交互时,使用UIView,不需要交互时,使用两者都可以
2:UIView和CALayer的渲染
下图可以说明view 和 layer之间是如何渲染的
- 界面触发的方式有两种
==> 通过loadView中子View的drawRect方法触发:会回调CoreAnimation中监听Runloop的BeforeWaiting的RunloopObserver
,通过RunloopObserver来进一步调用CoreAnimation内部的CA::Transaction::commit()
,进而一步步走到drawRect
方法
==> 用户点击事件触发:唤醒Runloop,由source1处理(__IOHIDEventSystemClientQueueCallback)
,并且在下一个runloop里由source0转发给UIApplication(_UIApplicationHandleEventQueue)
,从而能通过source0里的事件队列来调用CoreAnimation内部的CA::Transaction::commit()
;方法,进而一步一步的调用drawRect
。
最终都会走到CoreAnimation中的CA::Transaction::commit()
方法,从而来触发UIView和CALayer的渲染 - 这时,已经到了CoreAnimation的内部,即调用
CA::Transaction::commit()
;来创建CATrasaction,然后进一步调用CALayer drawInContext:()
- 回调CALayer的Delegate(UIView),问UIView没有需要画的内容,即回调到
drawRect:
方法 - 在drawRect:方法里可以通过CoreGraphics函数或UIKit中对CoreGraphics封装的方法进行画图操作
- 将绘制好的位图交由CALayer,由OpenGL ES 传送到GPU的帧缓冲区
- 等屏幕接收到垂直信号后,就读取帧缓冲区的数据,显示到屏幕上
CoreAnimation
在苹果官方的描述中,Render、Compose,and animate visual elements
,CoreAnimationg中的动画只是一部分,它其实是一个复合引擎,主要的职责包括 渲染、构建和动画实现。
ios中CoreAnimation如图所示
- ios中基于CoreAnimation构建的框架有两个:UIKit和APPKit
- CoreAnimation 又是基于Metal 、CoreGraphics封装的
苹果为什么要基于UIView和CALayer提供两个平行的层级关系(UIKit 和APPKit)?
职责分离
,可以避免大量重复代码两个系统交互规则不一致
,虽然功能上类似,但实现上有显著区别
CoreAnimation中的渲染流水线
CoreAnimation中渲染的流程如图所示
主要分为两部分:
- CoreAnimation部分
- GPU部分
- App处理UIView、UIButton等载体的事件,然后通过CPU完成对显示内容的计算,并将计算后的图层进行打包,在下一次runloop时,发送到渲染服务器
-
Render Server中主要对收到的准备显示的内容进行解码,然后执行OpenGL等相关程序,并调用GPU进行渲染
==> Render Server 操作分析
GPU部分
- GPU中通过顶点着色器、片元着色器完成对显示内容的渲染,将结果存入帧缓存区
- GPU通过帧缓存区、视频控制器等相关部件,将其显示到屏幕上
二:卡顿监控
卡顿监控的方案一般有两种:
-
FPS监控
:为了保持流程的UI交互,App的刷新拼搏应该保持在60fps
左右,其原因是因为iOS
设备默认的刷新频率是60次/秒
,而1次刷新(即VSync
信号发出)的间隔是1000ms/60 = 16.67ms
,所以如果在16.67ms
内没有准备好下一帧数据,就会产生卡顿 -
主线程卡顿监控
:通过子线程监测主线程的RunLoop,判断两个状态(kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
)之间的耗时是否达到一定阈值
1. YYKit库
YYkit主要是使用CADisplayLink
绑定到runloop
上进行监听渲染次数。
2. 监听runloop
监听runloop
事物处理的时间,主要监听runloop的kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
这两个状态值,如果监听到了超过两秒
,说明页面产生了卡顿
。
微信检测工具matrix:就是利用了此原理。
3. ping runloop
三:卡顿的优化
1. 预排版
-
思路分析:开辟两个线程
- 网络请求的线程: 进行异步网络请求
- 预排版线程:提前计算好布局属性,比如根据内容动态设置高度、宽度等。
2. 预解码 & 预渲染
以UIImage为例,image是的赋值是先通过解压缩 -> 渲染 -> 显示
,一般在UIImageVIew.image = image
都是在主线程操作,造成一些界面的耗时,预解码和预渲染在异步线程
3. 按需加载
-
异步渲染内容到图片。
-
按照滑动速度按需加载内容。
4. 异步渲染
view 和 layer的区别
- view是通过layer驱动的
- view是可以交互,layer是不可以的
- view侧重于显示,layer是对内容的绘制
- view是layer的代理,view的显示是交给layer的display进行渲染的。
CPU层面的优化
-
1、尽量
用轻量级的对象
代替重量级的对象,可以对性能有所优化,例如 不需要相应触摸事件的控件,用CALayer
代替UIView
-
2、尽量减少对
UIView
和CALayer
的属性修改-
CALayer内部并没有属性,当调用属性方法时,其内部是通过运行时
resolveInstanceMethod
为对象临时添加一个方法,并将对应属性值保存在内部的一个Dictionary中,同时还会通知delegate、创建动画等,非常耗时 -
UIView
相关的显示属性,例如frame、bounds、transform等,实际上都是从CALayer映射来的,对其进行调整时,消耗的资源比一般属性要大
-
-
3、当有大量对象释放时,也是非常耗时的,尽量挪到后台线程去释放
-
4、尽量
提前计算视图布局
,即预排版
,例如cell的行高 -
5、
Autolayout
在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout带来的CPU消耗是呈指数上升的。所以尽量使用代码布局
。如果不想手动调整frame等,也可以借助三方库,例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit等
-
6、文本处理的优化:当一个界面有大量文本时,其行高的计算、绘制也是非常耗时的
- 1)如果对文本没有特殊要求,可以使用UILabel内部的实现方式,且需要放到子线程中进行,避免阻塞主线程
-
计算文本宽高:
[NSAttributedString boundingRectWithSize:options:context:]
-
文本绘制:
[NSAttributedString drawWithRect:options:context:]
-
- 2)自定义文本控件,利用
TextKit
或最底层的CoreText
对文本异步绘制。并且CoreText
对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整和绘制都需要计算一次)。CoreText直接使用了CoreGraphics占用内存小,效率高
- 1)如果对文本没有特殊要求,可以使用UILabel内部的实现方式,且需要放到子线程中进行,避免阻塞主线程
-
7、图片处理(解码 + 绘制)
-
1)当使用
UIImage
或CGImageSource
的方法创建图片时,图片的数据不会立即解码,而是在设置时解码(即图片设置到UIImageView/CALayer.contents
中,然后在CALayer
提交至GPU渲染前,CGImage
中的数据才进行解码)。这一步是无可避免
的,且是发生在主线程
中的。想要绕开这个机制,常见的做法是在子线程中先将图片绘制到CGBitmapContext
,然后从Bitmap
直接创建图片,例如SDWebImage
三方框架中对图片编解码的处理。这就是Image的预解码
-
当使用CG开头的方法绘制图像到画布中,然后从画布中创建图片时,可以将图像的
绘制
在子线程
中进行
-
-
8、图片优化
-
1)尽量使用
PNG
图片,不使用JPGE
图片 -
2)通过
子线程预解码,主线程渲染
,即通过Bitmap
创建图片,在子线程赋值image -
3)优化图片大小,尽量避免动态缩放
-
4)尽量将多张图合为一张进行显示
-
-
9、尽量
避免使用透明view
,因为使用透明view,会导致在GPU中计算像素时,会将透明view下层图层的像素也计算进来,即颜色混合
处理, -
10、
按需加载
,例如在TableView中滑动时不加载图片,使用默认占位图,而是在滑动停止时加载 -
11、少使用
addView
给cell
动态添加view
GPU层面优化
相对于CPU而言,GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并渲染,输出到屏幕上。
-
1、尽量
减少在短时间内大量图片的显示
,尽可能将多张图片合为一张显示
,主要是因为当有大量图片进行显示时,无论是CPU的计算还是GPU的渲染,都是非常耗时的,很可能出现掉帧的情况 -
2、尽量避免图片的尺寸超过
4096×4096
,因为当图片超过这个尺寸时,会先由CPU进行预处理,然后再提交给GPU处理,导致额外CPU资源消耗 -
3、尽量减少视图数量和层次,主要是因为视图过多且重叠时,GPU会将其混合,混合的过程也是非常耗时的
-
5、异步渲染,例如可以将cell中的所有控件、视图合成一张图片进行显示。
注:上述这些优化方式的落地实现,需要根据自身项目进行评估,合理的使用进行优化
注意