030*:界面优化(CoreImage、CoreGraphics、OpenGL ES、metal、CoreAnimation【CALayer】)(预排版,异步渲染,按需加载,动态添加控件,避免使用透明度和圆角,离屏渲染,异步渲染合成一张图片)

问题

(CoreImage、CoreGraphics、OpenGL ES、metal、CoreAnimation【CALayer】) 

1:

目录

1:卡顿的原理

2:卡顿监控

3:卡顿的优化

预备

 

正文

一:卡顿的原理

1:界面卡顿

通常来说,计算机中的显示过程是下面这样的,通过CPUGPU显示器协同工作来将图片显示到屏幕上

图像显示过程

 

最开始时,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部分
CoreAnimation部分
  • 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的kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting这两个状态值,如果监听到了超过两秒,说明页面产生了卡顿

  微信检测工具matrix:就是利用了此原理。

3. ping runloop

  创建一个轮询,进行询问runloop是否在忙碌状态
  滴滴检测工具 DoraemonKit

三:卡顿的优化

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、尽量减少对UIViewCALayer的属性修改

    • 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占用内存小,效率高
  • 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、少使用addViewcell动态添加view

GPU层面优化

相对于CPU而言,GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并渲染,输出到屏幕上。

  • 1、尽量减少在短时间内大量图片的显示,尽可能将多张图片合为一张显示,主要是因为当有大量图片进行显示时,无论是CPU的计算还是GPU的渲染,都是非常耗时的,很可能出现掉帧的情况

  • 2、尽量避免图片的尺寸超过4096×4096,因为当图片超过这个尺寸时,会先由CPU进行预处理,然后再提交给GPU处理,导致额外CPU资源消耗

  • 3、尽量减少视图数量和层次,主要是因为视图过多且重叠时,GPU会将其混合,混合的过程也是非常耗时的

  • 4、尽量避免离屏渲染,可以查看这篇文章

  • 5、异步渲染,例如可以将cell中的所有控件、视图合成一张图片进行显示。

注:上述这些优化方式的落地实现,需要根据自身项目进行评估,合理的使用进行优化

注意

 

引用

1:iOS-底层原理 34:界面优化方案

2:iOS界面优化原理及解决方案

3:OC底层探索(二十六)界面优化

posted on 2020-12-04 20:20  风zk  阅读(361)  评论(0编辑  收藏  举报

导航