参考YY大神:https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/
读大神的文章,也是自己学习巩固的过程。
屏幕显示图像的原理
CRT的电子琴按照综上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器或其他硬件会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号,简称 HSync;当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号,简称VSync.显示器通常以固定频率进行刷新, 这个刷新率就是VSync信号产生的频率。尽管现在的设备大部分是这个原理。
通常来说,计算机系统中CPU/GPU/显示器是协调工作的.CPU计算好显示内容提交到GPU,GPU渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照VSync 信号逐行读取帧缓冲区的数据,经过可能的转换传递给显示器显示.
显示系统通常会引入两个缓冲区,即双缓冲机制,在这种情况下,GPU会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器,但是当视频控制器未读取完成时,即屏幕内容刚显示一半时,GPU将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,出现画面撕裂现象.
为了解决这个问题,GPU通常有一个机制叫做垂直同步,简称 V-Sync, 当开启垂直同步后,GPU会等待显示器的 V-Sync信号发出后,才进行新的一帧渲染和缓冲区交换.这样就解决了撕裂现象,增加了画面流畅度,但需要消费更多的计算资源,也会带来延迟.
iOS设备始终使用双缓冲,并开启垂直同步, android直到4.1后,引入的,目前是三级缓存+垂直同步.
卡顿原因
在VSync信号到来后, 系统图形服务会通过 CADisplayLink 等机制通知 App, App主线程开始在CPU中计算显示内容, 比如视图的创建/布局计算/图片解码/文本绘制等. 随后CPU会将计算好的内容提交到 GPU去, 由GPU进行变换/合成/渲染. 随后GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync信号到来时显示到屏幕上. 由于垂直同步的机制, 如果在一个 VSync 时间内, CPU或者 GPU 没有完成内容提交, 则那一帧就会被丢弃, 等待下一次机会再显示,而这时显示屏会保留之前的内容不变,这就是界面卡顿的原因.
CPU 资源消耗原因和解决方案
对象创建
对象的创建会分配内存/调整属性/甚至还有读取文件等操作,比较消耗CPU资源.尽量用轻量的对象代替重量的对象,可以对性能有所优化. 比如 CALayer 比 UIView要轻量许多, 那么不需要响应触摸事件的控件,用CALayer 显示会更加合适. 如果对象不涉及UI操作,则尽量放到后台线程去创建, 但是包含了CALayer的控件, 都只能在主线程创建和操作.
通过Storyboard 创建视图对象时, 起资源消耗会比直接通过代码创建对象要大非常多, 在性能敏感的界面里, Storyboard 并不是一个好的技术选择.
尽量推迟对象创建的时间, 并把对象的创建分散到多个任务中去.尽量这实现起来.尽管这实现起来比较麻烦,并且带来的优势并不多, 但如果有能力做,还是要尽量尝试一下.如果对象可以复用.并且复用的代价比释放/创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用.
对象调整
CALayer内部并没有属性,当调用属性方法时, 它内部是通过运行时resolveInstanceMethod为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里, 同时还会通知delegate/创建动画等等, 非常消耗资源. UIView 的关于显示相关的属性 如 frame/bounds/transform 等实际上都是 CALayer 属性映射来的, 所以对UIView 的这些属性进行调整时, 消耗资源要远大于一般的属性. 对此你在应用中,尽量减少不必要的属性修改.
当视图层次调整时, UIView/CALayer 之间会出现很多方法调用与通知, 所以在优化性能时, 应该尽量避免调整视图层次/添加和移除视图.
对象销毁
当容器类持有大量对象时, 其销毁时资源的消耗就非常明显. 如果对象可以放到后台线程去释放,可以放到后台,或者考虑使用自动释放池
布局计算
视图布局也耗CPU资源.如果能在后台线程提前计算好视图布局/并且对视图布局进行缓存.那么这个地方基本就不会产生性能问题了.涉及到UIView.frame/bounds/center等属性的调整,尽量提前计算好布局,需要时一次性调整好对应属性,不要多吃/频繁的计算和调整这些属性.
Autolayout
Autolayout是苹果推荐的,但是对于复杂视图来说不建议使用
文本计算
如果一个界面包含大量文本 如微博/微信朋友圈, 文本的高度计算会占用很大一部分资源, 并且不可避免.
对于文本显示没有很高要求::用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。
用CoreText 绘制文本, 那就可以先生成CoreText 排版对象, 然后自己计算了, 并且CoreText 对象还能保留稍后绘制使用.
文本渲染
屏幕能看到的所有文本内容控件,包括UIWebView, 在底层都是通过 CoreText 排版/ 绘制为Bitmap显示的.
当显示大量文本时,就自定义文本控件,用TextKit或者最底层的 CoreText 对文本异步绘制. 尽管这实现起来非常麻烦, 但其带来的优势也非常大, CoreText 对象创建好后, 能直接获取文本的宽高等信息, 避免了多次计算 (调整 UILabel 大小.绘制时内部再算一遍); CoreText 对象占用内存较少, 可以缓存下来进行渲染.
图片解码
当你用 UIImage 或 CGImageSource 的那几个方法创建图片时, 图片数据并不会立刻解码. 图片设置到 UIImageView 或者 CALayer.contents 中去, 并且 CALayer 被提交到 GPU前, CGImage 中的数据才会得到解码. 发生在主线程,不可避免. 如果想绕开,常见的做法就是在后台线程先把图片绘制到 CGBitmapContext中, 然后从 Bitmap 直接创建图片. 目前常用的网络图片库都自带这个功能.
图像的绘制
通常指用CG开头的方法把图像绘制到画布上, 然后从画布创建图片并显示这样一个过程.最常见的地方就是[UIView drawRect:]里面. 由于 CoreGraphic 方法通常都是线程安全的, 所以图像的绘制可以很容易的放到后台线程进行.
GPU 资源消耗原因和解决方案
GPU能干的事情比较单一: 接收提交的纹理 Texture 和顶点描述 (三角形),应用变换(transform),混合并渲染, 然后输出到屏幕上. 通常你所能看到的内容, 主要也就是纹理(图片) 和形状 (三角模拟的矢量图形)两类.
纹理的渲染
所有的 Bitmap, 包括图片/文本/栅格化内容, 最终都要由内存提交到显存, 绑定为 GPU Texture. 不论是提交到显存的过程, 还是 GPU 调整和渲染 Texture 的过程, 都要消耗不少 GPU资源. 当在较短时间显示大量图片时(TableView 存在非常多的图片并且快速滑动时),CPU占用率很低, GPU 占用非常高, 界面仍然会掉帧,.避免这种情况的方法只能是尽量减少在短时间内大量图片的显示, 尽可能将多张图片合成为一张进行显示.
当图片过大,超过最大纹理尺寸时,需要由CPU进行预处理.
视图的混合 Composing
当多个视图 或CALayer 重叠在一起显示时, GPU会首先把他们混合到一起.如果视图结构过于复杂, 混合的过程也会消耗很多 GPU资源. 为了减轻GPU消耗,应当尽量减少视图数量和层次, 并在不透明的视图里标明 opaque 属性以避免无用的 AIpha 通道合成. 当然也可以多个视图渲染为一张图片显示.
图形的生成
CALayer 的border/圆角/阴影/遮罩(mask), CASharoLayer 的矢量图形显示, 通常会触发离屏渲染 (offscreen rendering), 而离屏渲染通常发生在 GPU中. 当一个列表视图中出现大量圆角的CALayer,并且快速滑动时, 可以观察到GPU资源已经占满, CPU消耗很少.这是界面仍能滑动,但是帧数会降低.开启CALayer.shouldRasterize 属性,把离屏渲染操作转嫁到CPU上.或者死用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果.
最彻底的方法:把需要显示的图形在后台线程绘制为图片, 避免使用圆角/阴影/遮罩等属性.