界面优化
图片显示到屏幕上是CPU与GPU的协作完成
- CPU: 计算视图frame,图片解码,需要绘制纹理图片通过数据总线交给GPU
- GPU: 纹理混合,顶点变换与计算,像素点的填充计算,渲染到帧缓冲区。
- 时钟信号:垂直同步信号V-Sync / 水平同步信号H-Sync。
-
iOS设备双缓冲机制:显示系统通常会引入两个帧缓冲区,双缓冲机制
二.图片加载的工作流程
- 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
- 然后将生成的 UIImage 赋值给 UIImageView
- 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
- 在主线程的下一个 runloop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
- 分配内存缓冲区用于管理文件 IO 和解压缩操作;
- 将文件数据从磁盘读到内存中;
- 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
- 最后 Core Animation 中CALayer使用未压缩的位图数据渲染 UIImageView 的图层。
- CPU计算好图片的Frame,对图片解压之后.就会交给GPU来做图片渲染
- 渲染流程:
- GPU获取获取图片的坐标
- 将坐标交给顶点着色器(顶点计算)
- 将图片光栅化(获取图片对应屏幕上的像素点)
- 片元着色器计算(计算每个像素点的最终显示的颜色值)
- 从帧缓存区中渲染到屏幕上
我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。
卡顿监控
FPS监控:为了保持流程的UI交互,App的刷新拼搏应该保持在60fps左右,其原因是因为iOS设备默认的刷新频率是60次/秒,而1次刷新(即VSync信号发出)的间隔是 1000ms/60 = 16.67ms,所以如果在16.67ms内没有准备好下一帧数据,就会产生卡顿
主线程卡顿监控:通过子线程监测主线程的RunLoop,判断两个状态(kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting)之间的耗时是否达到一定阈值
界面优化
CPU层面的优化
1、尽量用轻量级的对象代替重量级的对象,可以对性能有所优化,例如 不需要相应触摸事件的控件,用CALayer代替UIView
2、尽量减少对UIView和CALayer的属性修改
- 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下层图层的像素也计算进来,即颜色混合处理,可以参考六、OpenGL 渲染技巧:深度测试、多边形偏移、 混合这篇文章中提及的混合
10、按需加载,例如在TableView中滑动时不加载图片,使用默认占位图,而是在滑动停止时加载
11、少使用addView 给cell动态添加view
GPU层面优化
1、尽量减少在短时间内大量图片的显示,尽可能将多张图片合为一张显示,主要是因为当有大量图片进行显示时,无论是CPU的计算还是GPU的渲染,都是非常耗时的,很可能出现掉帧的情况
2、尽量避免图片的尺寸超过4096×4096,因为当图片超过这个尺寸时,会先由CPU进行预处理,然后再提交给GPU处理,导致额外CPU资源消耗
3、尽量减少视图数量和层次,主要是因为视图过多且重叠时,GPU会将其混合,混合的过程也是非常耗时的
4、尽量避免离屏渲染,可以查看这篇文章四、深入剖析【离屏渲染】原理
5、异步渲染,例如可以将cell中的所有控件、视图合成一张图片进行显示。可以参考Graver三方框架