APP的性能监控包括: CPU 占用率
、 内存使用情况
、网络状况监控
、启动时闪退
、卡顿
、FPS
、使用时崩溃
、耗电量监控
、流量监控
等等。
一、CPU占有率
我们都知道,我们的APP在运行的时候,会对应一个Mach Task
,而Task下可能有多条线程同时执行任务,每个线程都是作为利用CPU的基本单位。所以我们可以通过获取当前Mach Task
下,所有线程占用 CPU 的情况,来计算APP的 CPU 占用率。
二、内存
虽然现在的手机内存越来越大,但毕竟是有限的,如果因为我们的应用设计不当造成内存过高,可能面临被系统“干掉”的风险,这对用户来说是毁灭性的体验。
三、启动时间
1.App启动过程
- 解析Info.plist
- 加载相关信息,例如如闪屏
- 沙箱建立、权限检查
- Mach-O加载
- 如果是胖二进制文件,寻找合适当前CPU类别的部分
- 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
- 定位内部、外部指针引用,例如字符串、函数等
- 执行声明为attribute((constructor))的C函数
- 加载类扩展(Category)中的方法
- C++静态对象加载、调用ObjC的 +load 函数
- 程序执行
- 调用main()
- 调用UIApplicationMain()
- 调用applicationWillFinishLaunching
扩展
- Mach-O文件是什么:
- Mach-O 是 Mach object 文件格式的缩写,它是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。作为 a.out 格式的替代品,Mach-O 提供了更好的扩展性,并提升了符号表中信息的访问速度。
- 常见的Mach-O文件类型
- MH_OBJECT
目标文件(.o)
静态库文件(.a),静态库其实就是N个.o合并在一起
- MH_EXECUTE:可执行文件
.app/xx- MH_DYLIB:动态库文件
.dylib
.framework/xx
- MH_DYLINKER:动态链接编辑器
/usr/lib/dyld
- MH_DSYM:存储着二进制文件符号信息的文件
.dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩溃信息)
- 目标文件类型
- Mach-O 文件
Mach-O 文件包含一种架构(i386、x86_64、arm64 等等)的对象代码
- 通用二进制文件
也叫作胖文件,胖文件可能包含若干包含不同架构(i386、x86_64、arm、arm64 等等)对象代码的对象文件
毫无疑问移动应用的启动时间是影响用户体验的一个重要方面,那么我们究竟该如何通过启动时间来衡量一个应用性能的好坏呢?启动时间可以从冷启动和热启动两个角度去测量:
冷启动:指的是应用尚未运行,必须加载并构建整个应用,完成初始化的工作,冷启动往往比热启动耗时长,而且每个应用的冷启动耗时差别也很大,所以冷启动存在很大的优化空间,冷启动时间从applicationDidFinishLaunching:withOptions:方法开始计算,很多应用会在该方法对其使用的第三方库初始化。
热启动:应用已经在后台运行(常见的场景是用户按了 Home 按钮),由于某个事件将应用唤醒到前台,应用会在 applicationWillEnterForeground: 方法接收应用进入前台的事件
APP的启动时间,直接影响用户对你的APP的第一体验和判断。如果启动时间过长,不单单体验直线下降,而且可能会激发苹果的watch dog机制kill掉你的APP,那就悲剧了,用户会觉得APP怎么一启动就卡死然后崩溃了,不能用,然后长按APP点击删除键。(Xcode在debug模式下是没有开启watch dog的,所以我们一定要连接真机测试我们的APP)
t(App 总启动时间) = t1(
main()
之前的加载时间 ) + t2(main()
之后的加载时间 )。
- t1 = 系统的 dylib (动态链接库)和 App 可执行文件的加载时间;
- t2 =
main()
函数执行之后到AppDelegate
类中的applicationDidFinishLaunching:withOptions:
方法执行结束前这段时间。
所以我们对APP启动时间的获取和优化都是从这两个阶段着手,下面先看看main()
函数执行之前如何获取启动时间。
2.衡量main()函数执行之前的耗时
对于衡量main()之前也就是time1的耗时,苹果官方提供了一种方法,即在真机调试的时候,勾选DYLD_PRINT_STATISTICS
选项(如果想获取更详细的信息可以使用DYLD_PRINT_STATISTICS_DETAILS
),如下图:
输出结果如下:
Total pre-main time: 34.22 milliseconds (100.0%)
dylib loading time: 14.43 milliseconds (42.1%)
rebase/binding time: 1.82 milliseconds (5.3%)
ObjC setup time: 3.89 milliseconds (11.3%)
initializer time: 13.99 milliseconds (40.9%)
slowest intializers :
libSystem.B.dylib : 2.20 milliseconds (6.4%)
libBacktraceRecording.dylib : 2.90 milliseconds (8.4%)
libMainThreadChecker.dylib : 6.55 milliseconds (19.1%)
libswiftCoreImage.dylib : 0.71 milliseconds (2.0%)
系统级别的动态链接库,因为苹果做了优化,所以耗时并不多,而大多数时候,t1的时间大部分会消耗在我们自身App中的代码上和链接第三方库上。
所以我们应如何减少main()调用之前的耗时呢,我们可以优化的点有:
- 减少不必要的framework,特别是第三方的,因为动态链接比较耗时;
- check framework应设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查;
- 合并或者删减一些OC类,关于清理项目中没用到的类,可以借助AppCode代码检查工具:
- 删减一些无用的静态变量
- 删减没有被调用到或者已经废弃的方法
- 将不必须在+load方法中做的事情延迟到+initialize中
- 尽量不要用C++虚函数(创建虚函数表有开销)
3.衡量main()函数执行之后的耗时
第二阶段的耗时统计,我们认为是从main ()
执行之后到applicationDidFinishLaunching:withOptions:
方法最后,那么我们可以通过打点的方式进行统计。 Objective-C项目因为有main文件,所以我么直接可以通过添加代码获取:
// 1. 在 main.m 添加如下代码:
CFAbsoluteTime AppStartLaunchTime;
int main(int argc, char * argv[]) {
AppStartLaunchTime = CFAbsoluteTimeGetCurrent();
.....
}
// 2. 在 AppDelegate.m 的开头声明
extern CFAbsoluteTime AppStartLaunchTime;
// 3. 最后在AppDelegate.m 的 didFinishLaunchingWithOptions 中添加
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"App启动时间--%f",(CFAbsoluteTimeGetCurrent()-AppStartLaunchTime));
});
main函数之后的优化:
- 尽量使用纯代码编写,减少xib的使用;
- 启动阶段的网络请求,是否都放到异步请求;
- 一些耗时的操作是否可以放到后面去执行,或异步执行等。
四、FPS
通过维基百科我们知道,FPS
是Frames Per Second
的简称缩写,意思是每秒传输帧数,也就是我们常说的“刷新率(单位为Hz)。
FPS
是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的画面就会愈流畅,FPS
值越低就越卡顿,所以这个值在一定程度上可以衡量应用在图像绘制渲染处理时的性能。一般我们的APP的FPS
只要保持在 50-60之间,用户体验都是比较流畅的。
苹果手机屏幕的正常刷新频率是每秒60次,即可以理解为FPS
值为60。我们都知道CADisplayLink
是和屏幕刷新频率保存一致,所以我们是否可以通过它来监控我们的FPS
呢?!
使用CADisplayLink
监控界面的FPS
值,参考自YYFPSLabel:
五、卡顿
在了解卡顿产生的原因之前,先看下屏幕显示图像的原理。
1.屏幕显示图像的原理
现在的手机设备基本都是采用双缓存+垂直同步(即V-Sync)屏幕显示技术。
如上图所示,系统内CPU
、GPU
和显示器是协同完成显示工作的。其中CPU
负责计算显示的内容,例如视图创建、布局计算、图片解码、文本绘制等等。随后CPU
将计算好的内容提交给GPU
,由GPU
进行变换、合成、渲染。GPU
会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU
会直接将视频控制器的指针指向第二个容器(双缓存原理)。这里,GPU
会等待显示器的VSync
(即垂直同步)信号发出后,才进行新的一帧渲染和缓冲区更新(这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟)。
2.卡顿的原因
由上面屏幕显示的原理,采用了垂直同步机制的手机设备。如果在一个VSync
时间内,CPU
或GPU
没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。例如在主线程里添加了阻碍主线程去响应点击、滑动事件、以及阻碍主线程的UI绘制等的代码,都是造成卡顿的常见原因。
3.卡顿监控
卡顿监控一般有两种实现方案:
(1). 主线程卡顿监控。通过子线程监测主线程的
runLoop
,判断两个状态区域之间的耗时是否达到一定阈值。(2). FPS监控。要保持流畅的UI交互,App 刷新率应该当努力保持在 60fps。
FPS
的监控实现原理,上面已经探讨过这里略过。
在使用FPS
监控性能的实践过程中,发现 FPS
值抖动较大,造成侦测卡顿比较困难。为了解决这个问题,通过采用检测主线程每次执行消息循环的时间,当这一时间大于规定的阈值时,就记为发生了一次卡顿的方式来监控。 这也是美团的移动端采用的性能监控Hertz 方案,微信团队也在实践过程中提出来类似的方案--微信读书 iOS 性能优化总结。
六、耗电量监控
iOS 设备的电量一直是用户非常关心的问题。如果你的应用由于某些缺陷不幸成为电量杀手,用户会毫不犹豫的卸载你的应用,所以耗电也是 app 性能的重要衡量标准之一。然而事实上业内对耗电量的监控的方案都做的不太好,下面会介绍和对比业内已有的耗电量的监控方案。
电量获取三种方案对比如下:
七、网络监控
网络监控一般通过 NSURLProtocol 和代码注入(Hook)这两种方式来实现,由于 NSURLProtocol 作为上层接口,使用起来更为方便,因此很自然选择它作为网络监控的方案,但是 NSURLProtocol 属于 URL Loading System 体系中,应用层的协议支持有限,只支持 FTP,HTTP,HTTPS 等几个应用层协议,对于使用其他协议的流量则束手无策,所以存在一定的局限性。监控底层网络库 CFNetwork 则没有这个限制。
下面是网络采集的关键性能指标:
- TCP 建立连接时间
- DNS 时间
- SSL 时间
- 首包时间
- 响应时间
- HTTP 错误率
- 网络错误率
拓展专题(后续补充)
启动优化
Instruments工具使用
包体积优化
参考文章
iOS开发--APP性能检测方案汇总(一)
iOS 性能监控方案(上篇)
iOS 性能监控方案(下篇)