第二篇:Objective-C 知识回顾的UI视图部分之二

2.1.图像显示原理

“图像显示原理”对于 iOS 开发工程师来讲是比较深层次的问题,说这个主要是为了后续理解 “UI 卡顿掉帧原因讲解”做一个技术上的铺垫和技术基础的准备。

图像显示过程原理

图像显示过程原理解释:

  • CPUGPU 都是通过总线连接起来的。
  • CPU 中会生成位图,然后在合适的时机经由总线传递给 GPU
  • GPU 拿到这个位图之后,会做一个图层的渲染包括纹理的合成,之后会把这个结果放到帧缓冲区中。
  • 接下来由视频控制器根据 VSync 信号,在指定的时间之前去提取之前帧缓冲区的内容。
  • 然后最终显示到iPhone手机屏幕上。
对于 iOS 工程师来讲 UI 视图显示到屏幕的大致过程如何?

UI 视图显示到屏幕的大致过程?视图解释:

  • 首先当我们创建一个 UIView 的时候,它的显示部分是由 CALayer 来负责的。
  • CALayer 中有个 contents 属性,就是我们最终要绘制到屏幕上的位图,比如我们创建的是一个 UILable,那么 contents 里面放置的就是一个 “Hello world” 的文字位图。
  • 然后系统会在合适的时机回调给我们一个 drawRect: 方法,然后我们可以在此基础上,绘制一些我们自定义的内容。
  • 绘制好的这个位图,会由 Core Animation 这个框架,提交给 GPUOpenGL 的渲染管线,进行最终位图的渲染包括纹理的合成。
  • 然后显示到我们的iPhone屏幕上面。这个就是一个 UI 视图显示到屏幕的大致过程。
  • 虚线左侧这些内容都是发生在 CPU 上的,OpenGL 是发生的 GPU 上的。

2.2.UI卡顿、掉帧的原因

卡顿、掉帧原理图

卡顿、掉帧原理图解释:

  • 一般来讲页面滑动的流畅性是 60 FPS,也就是说一秒钟之内,需要有均匀的 60 帧画面出现。我们在人眼上看过去,就是流畅的效果。基于此,也就是说每隔 16.7 ms就需要产生一帧的画面。
  • 在这个 16.7 ms内就需要 CPU 和 GPU 来共同完成这一帧的数据。
  • 比如说 CPU 花费一定的时间来进行文本的布局、UI 的计算、视图的绘制、图片的解码,然后把最终产生的位图提交给 GPU。
  • 然后再由 GPU 对位图进行图层的合成以及纹理的渲染,然后准备好一帧的数据,在下一个 VSync 信号到来的时候,就可以显示画面。
  • 如果说 CPU 在文本的布局、UI 的计算、视图的绘制、图片的解码的时候,花费的时间比较多,那么给 GPU 的时间就会比较少,这样产生一帧数据的时间长度超过了 16.7ms,由于垂直同步机制(Vertical synchronization),则这一帧将会被丢弃,等待下一次 VSync 信号再显示,画面会停留在上一帧,就会产生 UI 的卡顿和掉帧的效果。
  • 总结一句话就是:“在规定的 16.7ms 内,在下一帧 VSync 信号到来之前,CPU 和 GPU 并没有协同完成下一帧画面的合成,画面会停留在上一帧,就会导致卡顿或者说掉帧”。

2.3.滑动优化方案

基于 CPU:

  • 对象创建、调整、销毁
  • 预排版(文本的布局、UI 的计算)
  • 预渲染(文本等异步绘制,图片编码解码等)

基于 GPU:

  • 纹理的渲染(避免离屏渲染)
  • 图层的合成(减少图层的复杂度)
CPU 资源消耗原因和解决方案:

对象创建:

  • 对象的创建会分配内存、调整属性、甚至读取文件等操作,比较消耗资源。
  • 尽量用轻对象代替重量对象,可以对性能有所优化:比如 CALayer 比 UIView 要轻量很多。
  • 如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓冲池里复用。

对象调整:

  • 对象的调整也经常是消耗 CPU 资源的地方。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,尽量减少不必要的属性修改。(这里特别说一下 CALayer:CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。)
  • 当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。

对象销毁:

  • 对象的销毁虽然消耗资源不多,但是积累起来也是不容忽视。如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发个消息以免编译器警告,就可以让对象在后台线程销毁了。
NSArray *temp = self.array;
self.array = nil;
dispatch_async(queue, ^{
  [temp class];
});

UI 的计算:

  • 视图布局的计算是 App 中最为常见的消耗 CPU 资源的地方,如果能在后台线程提前计算好视图布局,并且对视图布局进行缓存,那么这个地方基本就不会产生性能了。
  • 无论通过何种技术对视图进行布局,其最终都会落到对 UIView.frame/bounds/center 等属性的调整上。上面也说过,对这些数据的调整非常消耗资源,所以尽量提前计算好布局,在需要时一次性调整好对应属性,而不要多次、频繁的计算和调整这些属性。

Autolayout:

  • Autolayout 是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是 Autolayout 对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout 带来的 CPU 消耗会呈指数级上升。具体数据可以看这个文章:http://pilky.me/36/。 如果你不想手动调整 frame 等属性,你可以用一些工具方法替代(比如常见的 left/right/top/bottom/width/height 快捷属性),或者使用 ComponentKit、AsyncDisplayKit 等框架。

文本的布局

  • 如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。

图片的解码

  • 当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。

图像的绘制

  • 图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。一个简单异步绘制的过程大致如下(实际情况会比这个复杂得多,但原理基本一致):
- (void)display {
   dispatch_async(backgroundQueue, ^{
       CGContextRef ctx = CGBitmapContextCreate(...);
       // draw in context...
       CGImageRef img = CGBitmapContextCreateImage(ctx);
       CFRelease(ctx);
       dispatch_async(mainQueue, ^{
           layer.contents = img;
       });
   });
}
GPU 资源消耗原因和解决方案:

相对于 CPU 来说,GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。

纹理的渲染:

  • 所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是 GPU 调整和渲染 Texture 的过程,都要消耗不少 GPU 资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。

视图的混合 (Composing):

  • 当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。

图形的生成:

  • CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。

2.4.UIView 的绘制原理

UIView 绘制流程
CALayer 绘制流程

异步绘制

  • 代理负责生成对应的 bitmap
  • 设置该 bitmap 作为 layer.contents 属性的值
异步绘制的流程
项目传送门:UIEventDemo(注意观察日志输出)
回顾系列文章下一篇:Objective-C 知识回顾之语言特性
回顾系列文章上一篇:Objective-C 知识回顾的UI视图部分之一

推荐阅读更多精彩内容