iOS 中的保持页面流畅的技巧

产生卡顿的原因
屏幕显示图像的原理

iOS 中的保持页面流畅的技巧_第1张图片

在通产情况下.计算机中的CPU和GPU,以及显示器是以上面的这种方式来进行工作的.CPU计算好显示内容,然后将这些内容提交到GPU当中,GPU经过渲染完成后将渲染的结构放入帧缓冲区,随后视频控制器会按照VSync信号来进行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示.

在最简单的情况下,帧缓冲区只有一个,这时帧缓冲区的读取和刷新都会有比较到的效率问题.为了解决效率问题,显示系统通常会引入两个缓冲区,即双缓冲机制.在这种情况情况下,GPU会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU会之间把视频控制器的指针直线第二个缓冲期内.如此一来效率会有很大的提升.

双缓冲虽然能够解决效率的问题,但会引入一个新的问题.当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如:

iOS 中的保持页面流畅的技巧_第2张图片

为了解决这个问题,GPU通常有一个机制叫做垂直同步(简写也是V-Sync),当开启垂直同步后,GPU会等待显示器的VSync信号发出后,才进行新的一帧渲染和缓冲区更新.这样能解决换面撕裂现象,也增加了换面流畅度,但需求消费更多的计算资源,也会带来部分延迟.

在目前的主流的移动设备中出现的是什么情况?iOS的设备中会始终使用双缓存,并开启垂直同步.而安卓设备知道4.1版本后,Google才开始引用这种机制,目前安卓系统是三缓存+垂直同步.

解决方案

iOS 中的保持页面流畅的技巧_第3张图片

在VSync信号到来后,系统图形服务会通过 CADisplayLink 等机制通知APP,APP主线程开始在CPU 中计算显示内容,比如视图的创建,布局计算,图片解码,文本绘制等.随后CPU 会将计算好的内容提交到GPU上去,再由GPU进行变换,合成,渲染.随后GPU 会把渲染的结果提价到帧缓冲区中,等待下一次VSync 信号到来显示到屏幕上.由于垂直同步的机制,如果在一个VSync时间内,CPU或者是GPU没有完成内容的提交,则那一帧就会被丢弃,等待下一次机会在显示,而这时显示屏幕就会保留之前的内容不变.这就是界面卡顿的现象.

从图中可以看到,CPU和GPU不论哪个阻碍了显示流程,都会在成掉帧现象.所以开发时,也需要分别对CPU和GPU压力进行评估和优化.

CPU 资源消耗原因和解决方案

  • 对象创建

对象的创建会分配内存,调整属性,甚至还有读取文件等操作,比较消耗CPU资源.尽量用轻量级的对象代替重量级的对象,可以对性能有所优化.比如在使用CALayer 的时候就比 UIView 要轻量许多,那么不需要响应触摸时间的控件,用CALayer 显示会更加合适.如果对象不涉及到UI操作,则尽量放到后台线程去创建,但可惜的是包含有CALayer的控件,都只能在主线程创建和操作.通过Storyboard创建视图对象时,其资源消耗的也会比直接通过代码创建对象要大非常多,在性能敏感的界面里,Storyboard 并不是一个好的技术选择.

尽量推迟对象创建的时间,并把对象的创建分散到多个任务中去.尽管这是吸纳起来比较麻烦,并且带来的有时并不多,但如果有能力去做,还是要尝试一下.如果对象可是复用,并且复用的代价比释放,创建新对象要小,那么这类对象应当放到一个缓存池复用.

  • 对象调整
    对象的调整也经常消耗CPU资源的地方.CALayaer:CALayer内部并没有属性,当调整属性方法时,它内部是通过运行时resolveInstanceMethod为对象临时添加一个方法,并把对应属性值保存到内部的一个Dictionary里,同时还会通知Delegate ,创建动画等等.这是非常消耗资源的 .UIView 的关于显示相关的属性(比如frame/bounds/transform)等实际上都是CALayer属性映射出来的,所以对UIView的这些属性进行调整时,消耗的资源要远大于一般的属性.对此在应用中,应该尽量减少不必要的属性修改.

当视图层次调整时,UIView,CALayer 之间会出现很多的方法调整与通知,所以在优化性能时,应该尽量避免调整视图层次,添加和移除视图.

  • 对象销毁
    对象的销毁虽然消耗的资源不多,但是一旦积累起来也是不容易进行忽视的.通常当容器类持有大量的对象时,在进行销毁时所累积的资源消耗也会非常大.同样的,如果对象可以放到后台线程中去释放,那就挪到后台线程去.这里有个小的Tip: 把对象捕获到block中,然后再扔到后台队列去随便发个消息以避免编辑器警告,就可以让对象下后台线程销毁了.
NSArray *temp = self.array;
self.array = nil;
dispatch_async(queue, ^{
             [temp class];
});
  • 布局计算
    视图布局的计算是APP中最为常见的消耗CPU资源的地方.如果能在后台线程提前计算好视图布局,并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了.

不论通过何种技术对视图进行布局,最终都会落到对 UIView.frame/bouonds/center等属性的调整上.对这些非常消耗资源的属性,要尽量提前计算好布局,只在需要时一次性调整好对应的属性,而不要多次,频繁的计算和调整这些属性.

  • AUTOLAHYOUT

Autolayout是苹果本身提倡的技术,在大部分情况下也会提升开发效率,但所出现的问题,就是Autolayout对于复杂视图来说常常会产生严重的性能为题,并且随着视图的增加,这些问题也会逐渐进行积累.所以随着Autolayout带来的CPU消耗,也会呈现指数的上升.可以通过(比如常见的 left / right / top / bottom / width / height)这些属性来代替,或是使用框架来代替.

  • 文本计算

如果一个界面中包含了大量的文本,文本的宽高计算会占用很大的资源消耗,这些也都是不可避免的.如果没有特殊的要求,可以使用UIlabel 内部的实现方式,用[NSAttributedString boundingRectWithSize:options:context:]来进行文本宽高,用-[NSAttributedString drawWithRect:options:context:]来绘制文本.尽管这两个方法性能不错,但是仍旧需要放到后台线程进行以避免阻塞主线程.

如果使用CoreText绘制文本,那就可以先生成CoreText排版对象,然后自己计算,并且在使用CoreText对象还能保留以供以后绘制再次使用.

  • 文本渲染
    屏幕上能够看到的所有文本内容控件,包括UIWebView,在底层都是通过CoreText 来进行排版绘制,然后在Bitmap进行显示.常见的文本控件(CoreText, UITextView)等.但是这些控件排版和绘制都是在主线程进行的,当显示大量文本时,CPU的压力就会非常大.对此解决方案只有一个,那就是使用自定义文本控件,用TextKit或者是最底层的CoreText对文本进行绘制.尽管看起来这会非常的麻烦,但其所带来的优势也是非常大的,CoreText对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整UILabel大小时再计算一遍,UIlabel 绘制时内部在进行计算); CoreText 对象占用内存较少,可以缓存先来以备稍后多次渲染.

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

  • 图像的绘制
    图像的绘制通常是指那些以CG开头的方法把图形绘制到画布中,然后从画布创建图片并显示这样的一个过程.这个最常见的地方就是[UIView drawRect:]里面了.由于CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行.一个简单异步绘制的过程大致如下.

- (void)display{
      dispatch_async(backgroundQueue, ^{
              CGontextRef 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 资源.当在较短时间显示大量图片的时候(比如用UICollectionView 来显示图片,并且上下进行滑动的时候),CPU占用率要比GPU的占用率要低,界面仍然会出现掉帧的现象.避免这种情况的发生只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示.

当图片过大,超多GPU 的最大纹理尺寸时,图片需要先由CPU进行预处理,这对CPU和GPU来实说都会带来额外的资源消耗.目前来说,尽量不要让图片和视图的大小超过iPhone 中的4096*4096.这个尺寸是iPhone纹理尺寸的上限.

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

  • ** 图形生成**
    CALayerborder,圆角,阴影,遮罩(mask),CASharpLayer的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在GPU中.当一个列表视图中出现大量圆角的CALayer,并且快速滑动时,可以观察到GPU资源已经沾满,而CPU资源消耗很少.这是界面仍然能正常滑动,但平均帧数转嫁到CPU上去.对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角覆盖到原本视图上面来模拟形同的视觉效果.把需要显示的图形在后台线程绘制为图片,避免使用圆角,阴影,遮罩等是比较彻底的结局方法.

你可能感兴趣的:(iOS 中的保持页面流畅的技巧)