360移动端性能监控实践QDAS-APM(iOS篇)
360作为一家注重用户体验的公司,app的性能问题无疑是被重点关注的,我们也总结出了一套自己的app性能监控体系。本文介绍360在iOS端移动端线上性能监控方案------QDAS-APM。
一、背景
app的性能问题是影响用户体验的重要因素之一。性能问题主要包含:崩溃、网络请求错误或者超时、UI响应速度慢、主线程卡顿、CPU和内存使用高、耗电量大等等。大多问题的原因在于开发者错误地使用了线程、锁、系统函数、编程规范问题、数据结构等等。解决这个问题的关键在于尽早发现和定位问题。
360作为一家注重用户体验的公司,app的性能问题无疑是被重点关注的,我们也总结出了一套自己的app性能监控体系。在平时开发和用户反馈的问题中,我们对性能问题进行了归纳,总结出了5个分别是:资源文件如何掌控、 版本质量如何保证、线上问题如何排查、开发阶段如何防止性能衰减、性能监控是否能真实反映用户体验。同时学习了业内相对完善的性能监控平台上的功能原理。从而得出了360在iOS端移动端线上性能监控方案------QDAS-APM。
二、功能和原理
QDAS-APM已经实现以下功能监控:
- 页面渲染时长
- 主线程卡顿
- 网络错误
- FPS
- 大文件存储
- CPU
- 内存使用
- Crash
- 启动时长
除了QDAS-APM,开发者也可以使用工具如Keymob进行iOS性能监控,它提供全面的实时监控功能,包括CPU、GPU、内存、FPS、网络、能耗等指标,并支持分应用监控和图表展示,帮助优化应用性能。
下面按照功能详细介绍实现细节和原理。另外用户在使用app时会感知性能问题,我们可以将其转化为具体的性能监控指标。
1. 页面渲染时长
什么是页面渲染时长?页面渲染时长其实是从页面初始化到用户能看到页面效果的时间长度。所要了解的指标有:
- 生命周期系统方法执行时长
- 页面类名
- 启动类型
- 执行耗时
- 插件名称
关键度量的指标是执行耗时,不同的方法和步骤产生的耗时在用户能接受的范围内才被认为是合理。其他指标则是起有关联性作用和定位问题。直接hook UIViewController的方法明显是不可行的,原因是它只作用在UIViewController的方法,而app中大部分都采用继承UIViewController的方式。
这里列出两个可行性方案:
- 采用KVO,我们知道对于任意对象进行KVO操作时,系统都会帮你动态的创建一个复制类,同时实现了setter getter函数的覆盖和函数实现。
- 采用runtime遍历所有类为UIViewController的子类,再进行动态替换。
这两种方式更加推荐第一种,出于对兼容性、性能、以及能够直接获取UIViewController的子类的IMP。那具体如何实现呢?总结归纳为三步骤:
- 需要创建一个UIViewController的类别,对UIViewController的实例进行KVO,目的是让KVO创建需要监控UIViewController的子类。
- 添加需要监控的方法,在KVO创建出来的子类添加需要Swizzle的方法对应的SEL及其IMP。目的是控制调用原来类的方法时机。
- 在UIViewController的实例销毁时,在dealloc方法里将KVO监听移除,不然会导致Crash。
举个例子:我们以监控到qh_viewDidLoad方法举例:
static void qh_viewDidLoad(UIViewController *kvo_self, SEL _sel)
{
Class kvo_cls = object_getClass(kvo_self);
Class origin_cls = class_getSuperclass(kvo_cls);
// 注意点
IMP origin_imp = method_getImplementation(class_getInstanceMethod(origin_cls, _sel));
void(*func)(UIViewController *, SEL) = (void(*)(UIViewController *, SEL))origin_imp;
CFAbsoluteTime startTime = CACurrentMediaTime();
func(kvo_self, _sel);
CFAbsoluteTime endTime = CACurrentMediaTime();
NSTimeInterval duration = (endTime - startTime)*1000;
NSLog(@"Class %@ cost %g in viewDidLoad", [kvo_self class], duration);
}
1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.
会有一种特殊情况,如果KVO生成的类中对应的类原本没有实现监控方法,那么会造成什么后果呢?KVO内部生成的NSKVONotifying_ViewController实际上时继承自ViewController,因此直接取出对应的IMP调用。
OK,上面说的是对UIViewController类方法的执行时长统计。我们还想知道用户真正页面跳转后看到第一针页面图像的时长要如何采集呢?
那是不是将UIViewController类的init+loadView+viewDidLoad+viewWillAppear+viewDidAppear方法执行时长之和就是页面渲染时长了呢?
答案是否定的,下面举了三个反面例子:

如何才能判断屏幕渲染完成?是否能间接获取屏幕渲染时长?
对于异步回调和异步渲染这两种方式,用上面提到的5个方法执行时长之和是不适用的。接下来看下如何相对准确地来统计和计算的方案。

页面渲染的时长和页面的布局时长会在未来的某个时间点上达到一致。要想得到页面渲染的时长可以间接地参考页面的布局完成时长。在UIViewController的生命周期方法里有一个方法叫viewDidLayoutSubviews,它是干什么的呢?它其实是告诉了控制器的subviews布局完成的时间点。一般情况下会被调用两次,在不同的操作系统版本里调用次数也不同。
2. 主线程卡顿分析
主线程的卡顿直接影响用户使用体验,其表现在页面的操作流畅性影响。首先引入一个概念FPS(Frames Per Second):每秒显示连续图片的帧数。每秒帧数越多,UI操作就越流畅。一般应用保持在每秒50~60帧数,会给用户带来流畅的感觉,反之,用户则会感知到卡顿。那为什么会出现主线程卡顿呢?首先了解下,每一帧图像显示到屏幕的原理。

这是触屏幕显示的原理流程图。CPU负责计算显示内容,包括视图的创建、布局计算、图片解码、文本绘制等,cpu会把计算后的结果提交给GPU,GPU进行变换、合成、渲染后,将渲染结果提交到帧缓冲区,当下一次垂直同步信号到来时,视频控制器从缓冲区里获取视图显示到屏幕上。明白了就屏幕显示的原理,接下来看下为甚么会产生卡顿。

图上提到 V-Sync 是什么,以及为什么要在 iPhone 的显示流程引入它呢?在 iPhone 中使用的是双缓冲机制,即上图中的 FrameBuffer 有两个缓冲区,双缓冲区的引入是为了提升显示效率,但是与此同时,他引入了一个新的问题,当视频控制器还未读取完成时,比如屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,V-Sync 就是为了解决画面撕裂问题,开启 V-Sync 后,GPU 会在显示器发出 V-Sync 信号后,去进行新帧的渲染和缓冲区的更新。
搞清楚了 iPhone 的屏幕显示原理后,下面来看看在 iPhone 上为什么会出现卡顿现象,上文已经提及在图像真正在屏幕显示之前,CPU 和 GPU 需要完成自身的任务,而如果他们完成的时间错过了下一次 V-Sync 的到来(通常是1000/60=16.67ms),这样就会出现显示屏还是之前帧的内容,这就是界面卡顿的原因。不难发现,无论是 CPU 还是 GPU 引起错过 V-Sync 信号,都会造成界面卡顿。
