一、背景
360是一家注重用户体验的公司,公司的口号是用户至上。在这么一个注重用户体验的氛围里,app的性能问题无疑是被重点关注的,同样也是造成用户流失的罪魁祸首之一。性能问题主要包含:崩溃、网络请求错误或者超时、UI响应速度慢、主线程卡顿、CPU和内存使用高、耗电量大等等。大多问题的原因在于开发者错误地使用了线程、锁、系统函数、编程规范问题、数据结构等等。解决这个问题的关键在于尽早发现和定位问题。
目前国内各大公司都有自己的一套app性能监控体系,360也不例外。在平时开发和用户反馈的问题中,对性能问题进行了归纳总结出了5个分别是:资源文件如何掌控、 版本质量如何保证、线上问题如何排查、开发阶段如何防止性能衰减、性能监控是否能真实反映用户体验。同时学习了业内相对完善的性能监控平台上的功能原理。从而得出了360在iOS端移动端线上性能监控方案——QDAS-APM。
二、功能和原理
QDAS-APM已经实现以下功能监控:
- 页面渲染时长
- 主线程卡顿
- 网络错误
- FPS
- 大文件存储
- CPU
- 内存使用
- Crash
- 启动时长
下面按照功能详细介绍实现细节和原理。另外用户在使用app时会感知性能问题,我们可以将其转化为具体的性能监控指标。
(1)页面渲染时长
什么是页面渲染时长,其实是从页面初始化到用户能看到页面效果的时间长度。所要了解的指标有生命周期系统方法执行时长、页面类名、启动类型、执行耗时和插件名称。关键度量的指标是执行耗时,不同的方法和步骤产生的耗时在用户能接受的范围内才被认为是合理。其他指标则是起有关联性作用和定位问题。直接hook UIViewController的方法明显是不可行的,原因是它只作用在UIViewController的方法,而app中大部分都采用继承UIViewController的方式。
这里列出两个可行性方案:
1、采用KVO,我们知道对于任意对象进行KVO操作时,系统都会帮你动态的创建一个复制类,同时实现了setter getter函数的覆盖和函数实现。
2、采用runtime遍历所有类为UIViewController的子类,再进行动态替换。
这两种方式更加推荐第一种,出于对兼容性、性能、以及能够直接获取UIViewController的子类的IMP。那具体如何实现呢?总结归纳为三步骤:
1、需要创建一个UIViewController的类别,对UIViewController的实例进行KVO,目的是让KVO创建需要监控UIViewController的子类。
2、添加需要监控的方法,在KVO创建出来的子类添加需要Swizzle的方法对应的SEL及其IMP。目的是控制调用原来类的方法时机。
3、在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);
}
会有一种特殊情况,如果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 信号,都会造成界面卡顿。
主线程卡顿监控的实现思路:开辟一个子线程,然后实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况,然后通过采集当前主线程的堆栈信息和相关指标。从而上报到服务端进行处理,最终生成报表。FPS 的刷新频率非常快,容易发生抖动,直接通过比较 FPS 来侦测卡顿是比较困难的。主线程卡顿监控也会发生抖动,所以提出综合方案,结合主线程监控、FPS 监控,以及 CPU 使用率等指标,作为判断卡顿的标准。
(3)网络监控
网络监控一般通过 NSURLProtocol 和代码注入(Hook)这两种方式来实现,由于 NSURLProtocol 作为上层接口,使用起来更为方便,NSURLProtocol 属于 URL Loading System 体系中,应用层的协议支持有限,只支持 FTP,HTTP,HTTPS 等几个应用层协议,对于使用其他协议的流量则束手无策,所以存在一定的局限性。监控底层网络库 CFNetwork 则没有这个限制。如果本地有https的证书验证也不适用于NSURLProtocol这种方式。容易引起业务数据丢失问题。
NSURLProtocol
上图是基于NSURLProtocol协议来实现的,通过继承自NSURLProtocol,并注册。通过代理和自身方法来得到网络请求相关的指标。
HOOK方式—NSProxy
NSProxy is an abstract superclass defining an API for objects
that act as stand-ins for other objects or for objects that don’t exist yet.
Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or
transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent
distributed messaging (for example, NSDistantObject) or for lazy instantiation of objects that are expensive to create.
上面的这段英文是 Apple 官方文档给 NSProxy 的定义,NSProxy 和 NSObject 一样都是根类,它是一个抽象类,可以通过继承它,并重写 -forwardInvocation: 和 -methodSignatureForSelector: 方法以实现消息转发到另一个实例。综上,NSProxy 的目的就是负责将消息转发到真正的 target 的代理类。
那为什么我们不用Method swizzling 替换方法需要指定类名?是因为 NSURLConnectionDelegate 和 NSURLSessionDelegate 是由业务方指定,通常来说是不确定,所以这种场景不适合使用 Method swizzling。使用 NSProxy 可以解决,具体实现:proxy delegate 替换 NSURLConnection 和 NSURLSession 原来的 delegate,当 proxy delegate 收到回调时,如果是要 hook 的方法,则调用 proxy 的实现,proxy 的实现最后会调用原来的 delegate;反之,则通过消息转发机制将消息转发给原来的 delegate。下图示意了整个操作流程。
通过对NSURLConnection、NSURLSession和CFNetwork这三个类中关键方法的hook来获取上报指标。具体hook哪些方法,请看下图。
将hook方法中得到的相关指标整理成需要的格式上报到服务端,服务端通过数据处理和拆分指标,汇总加计算生成最终的报表。
三、QDAS-APM在集成和使用上的便捷
由于sdk功能基本上采用的都是主动采集功能,无需二次开发,也无需额外引入系统库。所以在集成和使用上非常便捷。在sdk的集成上,只需要三步骤:
1、引入sdk库
2、引入sdk头文件
3、在app的didFinishLauchingWithOptions里初始化sdk,并传入appkey即可。
参考:
(360技术原创内容,转载请务必保留文末二维码,谢谢~)
关于360技术 360技术是360技术团队打造的技术分享公众号,每天推送技术干货内容
更多技术信息欢迎关注“360技术”微信公众号
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。