iOS监控:卡顿检测

前言

在很早之前就有过实现一套自己的iOS监控体系,但首先是instrument足够的优秀,几乎所有监控相关的操作都有对应的工具。二来,也是笔者没(lan)时(de)间(zuo),项目大多也集成了第三方的统计SDK,所以迟迟没有去实现。这段时间,因为代码设计上存在的缺陷,导致项目在iphone5s以下的设备运行时会出现比较明显的卡顿现象。虽然instrument足够优秀,但笔者更希望在程序运行期间能及时获取卡顿信息,因此开始动手自己的卡顿检测方案。

获取栈上下文

任何监控体系在监控到目标事件发生时,获取线程的调用栈上下文是必须的,问题在于如何挂起当前线程并且获取线程信息。好在网上有大神分享了足够多的资料供笔者查阅,让笔者可以站在巨人的肩膀上来完成这部分业务。

demo中获取调用栈代码重写自BSBacktraceLogger,在使用之前建议能结合下方的参考资料和源代码一起阅览,知其然知其所以然。栈是一种后进先出(LIFO)的数据结构,对于一个线程来说,其调用栈的结构如下:

调用栈上每一个单位被称作栈帧(stack frame),每一个栈帧由函数参数返回地址以及栈帧中的变量组成,其中Frame Pointer指向内存存储了上一栈帧的地址信息。换句话说,只要能获取到栈顶的Frame Pointer就能递归遍历整个栈上的帧,遍历栈帧的核心代码如下:

从栈帧中我们只能获取到调用函数的地址信息,为了输出上下文数据,我们还需要根据地址进行符号化,即找到地址所在的内存镜像,然后定位该镜像中的符号表,最后从符号表中匹配地址对应的符号输出。

符号化过程中包括不限于以下的数据结构:

Dl_info存储了包括路径名、镜像起始地址、符号地址和符号名等信息

提供了符号表的偏移量,以及元素个数,还有字符串表的偏移和其长度。更多堆栈的资料可以参考文末最后三个链接学习。符号化的核心函数lxd_dladdr如下:

整个符号化过程可以用下面的图表示

关于RunLoop

RunLoop是一个重复接收着端口信号和事件源的死循环,它不断的唤醒沉睡,主线程的RunLoop在应用跑起来的时候就自动启动,RunLoop的执行流程由下图表示:

CFRunLoop.c中,可以看到RunLoop的执行代码大致如下:

通过源码不难发现RunLoop处理事件的时间主要出在两个阶段:

  • kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting之间
  • kCFRunLoopAfterWaiting之后

监控RunLoop状态检测超时

通过RunLoop的源码我们已经知道了主线程处理事件的时间,那么如何检测应用是否发生了卡顿呢?为了找到合理的处理方案,笔者先监听RunLoop的状态并且输出:

运行之后输出的结果是滚动引发的Sources事件总是被快速的执行完成,然后进入到kCFRunLoopBeforeWaiting状态下。假如在滚动过程中发生了卡顿现象,那么RunLoop必然会保持kCFRunLoopAfterWaiting或者kCFRunLoopBeforeSources这两个状态之一。

为了实现卡顿的检测,首先需要注册RunLoop的监听回调,保存RunLoop状态;其次,通过创建子线程循环监听主线程RunLoop的状态来检测是否存在停留卡顿现象: 收到Sources相关的事件时,将超时阙值时间内分割成多个时间片段,重复去获取当前RunLoop的状态。如果多次处在处理事件的状态下,那么可以视作发生了卡顿现象

标记位检测线程超时

与UI卡顿不同的事,事件处理往往是处在kCFRunLoopBeforeWaiting的状态下收到了Sources事件源,最开始笔者尝试同样以多个时间片段查询的方式处理。但是由于主线程的RunLoop在闲置时基本处于Before Waiting状态,这就导致了即便没有发生任何卡顿,这种检测方式也总能认定主线程处在卡顿状态。

就在这时候寒神(南栀倾寒)推荐给我一套Swift的卡顿检测第三方ANREye,这套卡顿监控方案大致思路为:创建一个子线程进行循环检测,每次检测时设置标记位为YES,然后派发任务到主线程中将标记位设置为NO。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成NO。如果没有说明主线程发生了卡顿,无法处理派发任务:

事后发现在特定情况下,这种检测方式会出错:当主线程被async大量的执行任务时,每个任务执行时间小于卡顿时间阙值,即对操作无影响。这时候由于设置标志位的async任务位置过于靠后,导致子线程沉睡后未能成功设置,造成卡顿误报的现象。(ps:当然,实测结果是基本不可能发生这种现象)这套方案解决了上面监听RunLoop的缺陷。结合这套方案,当主线程处在Before Waiting状态的时候,通过派发任务到主线程来设置标记位的方式处理常态下的卡顿检测:

尾言

多数开发者对于RunLoop可能并没有进行实际的应用开发过,或者说即便了解RunLoop也只是处在理论的认知上。当然,也包括调用堆栈追溯的技术。本文旨在通过自身实现的卡顿监控代码来让更多开发者去了解这些深层次的运用与实践。

此外,上面两种检测方案可以兼并使用,甚至只使用后者进行主线程的卡顿检测也是可以的,本文demo已经上传:LXDAppFluecyMonitor

参考资料

深入了解RunLoop
移动端监控体系之技术原理
趣探 Mach-O:FishHook 解析
iOS中线程Call Stack的捕获和解析1-2

1 1 收藏 1 评论

关于作者:林欣达

iOS码农一枚 个人主页 · 我的文章 · 1 ·     

相关文章

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部