iOS 性能优化(一)

本文将从原理出发,解释卡顿发生的原理,然后会讲解项目中行之有效的几个优化点,以此作为日后的参考提醒。下面进入正题。

屏幕显示图像原理
这里写图片描述

我们知道,CRT显示器的显示原理是用电子枪扫描荧光屏来发光。如上图所示,电子枪按照从左到右,然后从上到下的顺序扫描。当电子枪换到新的一行准备进行扫描时,显示器会发出一个水平同步信号;而当一帧画面绘制完成后,电子枪回复到原位准备画下一帧前,显示器会发出一个垂直同步信号。垂直同步信号的作用一方面是通知显示器回到第一行行首位置,另外一方面,也通知显卡,准备输出下一帧画面。现在已经是液晶显示器的时代了,不再使用电子枪扫描了,但是原理还是类似的,水平同步信号和垂直同步信号还是一样被使用的。

计算机工作原理
这里写图片描述
CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等,将数据经过总线传给GPU;
GPU进行变换、合成、渲染,将数据经过总线提交给帧缓冲区(FrameBuffer);
帧缓冲区(FrameBuffer)等待垂直同步信号 VSync,然后将缓冲区的内容显示到屏幕上。

屏幕撕裂(Screen tearing)
这里写图片描述
当资源同时发生读写操作时,就会产生数据错乱,解决办法就是添加线程安全锁。同理,屏幕撕裂的产生原因及修复措施也类似。
上述的简单的屏幕显示原理其实会产生这样一个问题:假设我们的显卡速度很快,每秒生产的帧数肯定要超过显示器刷新率。那么在实际数据处理过程中,缓冲区的数据,在被输出之前,就被显卡不断的刷新重写。但是缓冲区并不是“先清空再写入数据”,这太没有效率,而是采用“新数据覆盖老数据”的方式。
假设这样一种情况,缓冲区已经有一副完整的帧画面(A帧),然后显卡生成了下一帧画面(B帧),新一帧的数据开始写入缓冲区,写到一半的时候,垂直同步信号来 了,于是缓冲区的数据被输出到显示器。但问题是,这时缓冲区的数据,是由一半A帧和一半B帧数据合成的。因此最终显示器上显示出来的画面就不是一副完整的 画面,这就是“画面撕裂”现象出现的原因。

解决屏幕撕裂
简单来说只要让帧缓冲区里的数据始终保持一副完整的画面就可以了。从技术角度出发,其实就是利用刚刚提到的垂直同步信号 VSync。
具体说起来就是,当显卡生成了一副完整画面并写入了帧缓冲区之后,暂停!然后开始等待垂直同步信号,当得到垂直同步信号后,再继续渲染下一帧写入缓冲区。这样就可以保证在缓冲区的数据始终是一副完整的画面,不会出现前后帧混合的问题。

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

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

CPU资源消耗原因及解决方式

  1. 提前布局

    提前布局可以说是最重要的优化点了。其实在从服务端拿到 JSON 数据的时候,关于视图的布局就已经确定了,包括每个控件的frame、cell的高度以及文本排版结果等等,在这个时候完全可以在后台线程计算并封装为对应的布局对象XXXTableViewCellLayout,每个cellLayout的内存占用并不是很多,所以直接全部缓存到内存中。当列表滚动到某个cell的时候,直接拿到对应的cellLayout配置这个cell的对应属性即可。当然,该有的计算是免不了的,只是提前算好并缓存,免去了在滚动的时候计算和重复的计算。

  2. 对象的创建

    对象的创建会分配内存、设置属性等,会消耗CPU资源。所以尽量使用轻量对象代替,比如能用CALayer的时候尽量不用UIView,敏感位置能不用IB尽量使用纯代码手写。推迟同一时间创建对象,推荐使用懒加载在需要使用时候创建对象。

  3. 对象的调整

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

  4. 对象的销毁

    当前类持有大量对象时候,其销毁时候的资源消耗就非常明显。建议创建销毁的异步队列,将需要销毁的对象放到队列中销毁。

  5. AutoLayout

    Autolayout 对于复杂视图来说常常会产生严重的性能问题,AutoLayout相对低效的原因是隐藏在底层的命名为”Cassowary“的约束求解系统,随着视图数量的增长,Autolayout 带来的 CPU 消耗会呈指数级上升,当Cell内约束超过25个的时候,会降低滑动的帧率。

  6. 文本的计算以及渲染

    UI中存在大量的对于文本高度的适配,可以参考:用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。解决办法是利用TextKit或者是CoreText自定义文本控件。

  7. 图片解码以及图像的绘制

    当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。一个最常见的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。

  8. 文件系统的调用

    NSFileManager获取一个目录获取文件信息,进行多次递归计算,stat几乎瞬间完成,NSFileManager耗时较长且消耗CPU。

GPU 资源消耗原因和解决方式

  1. 纹理的渲染

    所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是 GPU 调整和渲染 Texture 的过程,都要消耗不少 GPU 资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。当图片过大,超过 GPU 的最大纹理尺寸时,图片需要先由 CPU 进行预处理,这对 CPU 和 GPU 都会带来额外的资源消耗。目前来说,iPhone 4S 以上机型,纹理尺寸上限都是 4096×4096,更详细的资料可以看这里:iosres.com。所以,尽量不要让图片和视图的大小超过这个值。

  2. 视图的混合

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

  3. 纹理的渲染

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

  4. 其他
    数据分页加载、做好数据缓存、异步处理、避免离屏渲染

总结

性能优化这个东西其实很难形成一个具体的方案,为什么这么说?因为之所以称之为优化,是因为要在原有的代码基础上进行优化,原有的代码又有各式各样的原因导致需要依照现有代码来优化,而很难完全脱离现有的情况完全参照某一种的既定方案进行优化。假如说是完全参照某一种的方案优化的话,建议还是将某一个性能敏感的页面利用Texture进行完全重写,这样才能算是整体化一的利用了某一种方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章