前言
本文将从发现问题、解决问题和预防问题进行总结
如何发现性能问题
业务性能监控,是指在
App
本地,业务的开始和结束处打点上报,然后后台统计达到监控目的;卡顿监控。卡顿监控的实现一般有两种方案:
1)主线程卡顿监控。通过子线程监测主线程的 runLoop,判断两个状态区域之间的耗时是否达到一定阈值。具体原理和实现,这篇文章介绍得比较详细。
(2)FPS监控。要保持流畅的UI交互,App 刷新率应该当努力保持在 60fps。监控实现原理比较简单,通过记录两次刷新时间间隔,就可以计算出当前的 FPS。
- 在实际应用过程,无论是主线程监控,还是 FPS 监控,抖动都比较大。因此,一套综合的判断方法,结合了主线程监控、FPS监控,以及CPU使用率等指标,作为判断卡顿的标准。
性能问题的解决方法
- 优化业务流程。
性能优化看似高深,真正落到实处才会发现,最大的坑往往都隐藏在于业务不断累积和频繁变更之处。 - 合理的线程分配
由于GCD
实在太方便了,如果不加控制,大部分需要抛到子线程操作都会被直接加到global
队列,这样会导致两个问题:
(1). 开的子线程越来越多,线程的开销逐渐明显,因为开启线程需要占用一定的内存空间(默认的情况下,主线程占1M,子线程占用512KB)。
(2).多线程情况下,网络回调的时序问题,导致数据处理错乱,而且不容易发现。为此,我们项目定了一些基本原则。
- UI 操作和 DataSource 的操作一定在主线程。
- DB 操作、日志记录、网络回调都在各自的固定线程。
- 不同业务,可以通过创建队列保证数据一致性。
合理的线程分配,最终目的就是保证主线程尽量少的处理非UI操作,同时控制整个App的子线程数量在合理的范围内。
- 预处理和延时加载
预处理:是将初次显示需要耗费大量线程时间的操作,提前放到后台线程进行计算,再将结果数据拿来显示。
延时加载:是指首先加载当前必须的可视内容,在稍后一段时间内或特定事件时,再触发其他内容的加载。这种方式可以很有效的提升界面绘制速度,使体验更加流畅。(UITableView 就是最典型的例子)
这两种方法都是在资源比较紧张的情况下,优先处理马上要用到的数据,同时尽可能提前加载即将要用到的数据。
- 缓存
cache
可能是所有性能优化中最常用的手段,但也是我们极不推荐的手段。cache
建立的成本低,见效快,但是带来维护的成本却很高。如果一定要用,也请谨慎使用,并注意以下几点:
- 并发访问
cache
时,数据一致性问题。
-
cache
线程安全问题,防止一边修改一边遍历的 crash。 -
cache
查找时性能问题。 -
cache
的释放与重建,避免占用空间无限扩大,同时释放的粒度也要依实际需求而定。
- 使用正确的API
使用正确的 API,是指在满足业务的同时,能够选择性能更优的API。
- 选择合适的容器;
- 了解 imageNamed: 与 imageWithContentsOfFile:的差异(imageNamed: 适用于会重复加载的小图片,因为系统会自动缓存加载的图片,imageWithContentsOfFile: 仅加载图片)
- 缓存 NSDateFormatter 的结果。
- 寻找 (NSDate *)dateFromString:(NSString )string 的替换品。
//#include <time.h> time_t t; struct tm tm; strptime([iso8601String cStringUsingEncoding:NSUTF8StringEncoding], "%Y-%m-%dT%H:> %M:%S%z", &tm); tm.tm_isdst = -1; t = mktime(&tm); [NSDate dateWithTimeIntervalSince1970:t + [[NSTimeZone localTimeZone] secondsFromGMT]];
不要随意使用 NSLog().
当试图获取磁盘中一个文件的属性信息时,使用
[NSFileManager attributesOfItemAtPath:error:]
会浪费大量时间读取可能根本不需要的附加属性。这时可以使用stat
代替NSFileManager
,直接获取文件属性:#import <sys/stat.h> struct stat statbuf; const char *cpath = [filePath fileSystemRepresentation]; if (cpath && stat(cpath, &statbuf) == 0) { NSNumber *fileSize = [NSNumber numberWithUnsignedLongLong:statbuf.st_size]; NSDate *modificationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_mtime]; NSDate *creationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_ctime]; // etc }
如何预防性能问题
内存泄露检测工具
MLeakFinder是团队成员zepo在github开源的一款内存泄露检测工具,具体原理和使用方法可以参见这篇文章。在此之前,内存泄露引起的性能问题是很难被察觉的,只有泄露到了相当严重的程度,然后通过Instrument工具,不断尝试才得以定位。MLeakFinder能在开发阶段,把内存泄露问题暴露无遗,减少了很多潜在的性能问题。FPS/SQL性能监测工具条
该工具条是在DEBUG模式下,以浮窗的形式,实时展示当前可能存在问题的FPS次数和执行时间较长的SQL语句个数,是团队成员tower
的杰作。FPS监测的原理并不复杂,前文也有介绍,虽然并不百分百准确,但非常实用,因为可以随时查看FPS低于某个阈值时的堆栈信息,再结合当时的使用场景,开发人员使用起来非常便利,可以很快定位到引起卡顿的场景和原因。SQL语句的监测也非常实用.UI / DataSource主线程检测工具。
该工具是为了保证所有的UI的操作和 DataSource 操作一定是在主线程进行,同样是由tower同学贡献。实现原理是通过hook UIView
的-setNeedsLayout,-setNeedsDisplay,-setNeedsDisplayInRect
三个方法,确保它们都是在主线程执行。子线程操作UI可能会引起什么问题,苹果说得并不清楚,实际开发中我们遇到几种神奇的问题似乎都是跟这个有关。
app 突然丢动画,似乎 iOS 系统也有这个 bug。虽然没有确切的证据,但使用这个工具,改完所有的问题后,bug 也好了(不止一次是这样)。
UI 操作偶尔响应特别慢,从代码看没有任何耗时操作,只是简单的 push 某个 controller。
莫名的 crash,这当然是因为 UI 操作非线程安全引起的。
更多时候,子线程操作 UI 也并不一定会发生什么问题,也正因为不知道会发生什么,所以更需要我们警惕,这个工具替我们扫除了这些隐患。虽然,苹果表示,现在部分的 UI 操作也已经是线程安全了,但毕竟大部分还不是。DataSource 的监测是因为我们业务定下的原则,保证列表 DataSource 的线程安全。
卡顿产生的原因
界面卡顿的原因:在
VSync
信号到来后,系统图形服务会通过 CADisplayLink
等机制通知 App,App 主线程开始在CPU
中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU
去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次VSync
信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。在开发中,CPU和GPU中任何一个压力过大,都会导致掉帧现象,所以在开发时,也需要分别对
CPU和GPU压力进行评估和优化
。
CPU:加载资源,对象创建,对象调整,对象销毁,布局计算,
Autolayout
,文本计算,文本渲染,图片的解码, 图像的绘制(Core Graphics
)都是在CPU
上面进行的。
GPU:
GPU
是一个专门为图形高并发计算而量身定做的处理单元,比CPU
使用更少的电来完成工作并且GPU的浮点计算能力要超出CPU很多。
GPU的渲染性能要比CPU高效很多,同时对系统的负载和消耗也更低一些,所以在开发中,我们应该尽量让CPU负责主线程的UI调动,把图形显示相关的工作交给GPU来处理,当涉及到光栅化等一些工作时,CPU也会参与进来,这点在后面再详细描述。
相对于CPU来说,GPU能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合(合成)并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。
CPU 和 GPU 的协作:要在屏幕上显示视图,需要CPU和GPU一起协作,CPU计算好显示的内容提交到GPU,GPU渲染完成后将结果放到帧缓存区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
缓冲机制:
iOS
使用的是双缓冲机制。即GPU会
预先渲染好一帧放入一个缓冲区内(前帧缓存)
,让视频控制器读取,当下一帧渲染好后,GPU
会直接把视频控制器的指针指向第二个缓冲器(后帧缓存)。当你视频控制器已经读完一帧,准备读下一帧的时候,GPU
会等待显示器的VSync信号发出后,前帧缓存和后帧缓存会瞬间切换,后帧缓存会变成新的前帧缓存,同时旧的前帧缓存会变成新的后帧缓存。
iOS 保持界面流畅的技巧
LLDebugTool是一款针对开发者和测试者的调试工具
iOS 性能优化总结
iOS性能优化系列篇之“优化总体原则”
iOS应用UI线程卡顿监控
UITableView-FDTemplateLayoutCell - 缓存
绘制像素到屏幕上
iOS图形原理与离屏渲染
iOS 保持界面流畅的技巧
Advanced Graphics and Animations for iOS Apps(session 419)
使用 ASDK 性能调优 - 提升 iOS 界面的渲染性能
Designing for iOS: Graphics & Performance
iOS离屏渲染之优化分析
iOS视图渲染以及性能优化总结
iOS 离屏渲染
深刻理解移动端优化之离屏渲染
iOS 流畅度性能优化、CPU、GPU、离屏渲染
iOS 图形性能优化锦集
离屏渲染优化详解:实例示范+性能测试