CPU 监控
CPU是移动设备最重要的计算资源,如果CPU持续高负载运行,一方面会导致用户使用过程遭遇卡顿,另一方面也会使手机发热发烫,电量被快速消耗完,严重影响用户体验
避免之上情况出现,可通过监控应用的CPU占用率
Q:CPU占用率如何监控呢?
A:线程CPU是调度和分配的基本单位,而应用作为进程运行时,包含了多个不同的线程,如果能知道app里所有线程占用CPU的情况,也就能知道整个APP的CPU占用率。
iOS 是基于 Apple Darwin 内核,由 kernel、XNU 和 Runtime 组成,而 XNU 是 Darwin 的内核,它是“X is not UNIX”的缩写,是一个混合内核,由 Mach 微内核和 BSD 组成。Mach 内核是轻量级的平台,只能完成操作系统最基本的职责,比如:进程和线程、虚拟内存管理、任务调度、进程通信和消息传递机制。其他的工作,例如文件操作和设备访问,都由 BSD 层实现。
引用自:《OS X and iOS Kernel Programming》 中对Mac OS X 中进程子系统组成的概念图
iOS的线程技术也是基于Mach线程技术实现的,在Mach层中thread_basic_info结构体中发现了我们想要的东西
cpu_usage 对应线程的CPU使用率
任务(task)是一种容器(container)对象,虚拟内存空间和其他资源都是通过这个容器对象管理的,这些资源包括设备和其他句柄。严格地说,Mach 的任务并不是其他操作系统中所谓的进程,因为 Mach 作为一个微内核的操作系统,并没有提供“进程”的逻辑,而只是提供了最基本的实现。不过在 BSD 的模型中,这两个概念有1:1的简单映射,每一个 BSD 进程(也就是 OS X 进程)都在底层关联了一个 Mach 任务对象
Mach task 可以看作一个机器无关的 thread 执行环境的抽象
一个 task 包含它的线程列表。内核提供了 task_threads API 调用获取指定 task 的线程列表,然后可以通过 thread_info API 调用来查询指定线程的信息,thread_info API 在 thread_act.h中定义
task_threads 将 target_task 任务中的所有线程保存在 act_list 数组中,数组中包含 act_listCnt 个条目
thread_info 查询 flavor 指定的 thread 信息,将信息返回到长度为 thread_info_outCnt 字节的 thread_info_out 缓存区中
Memory监控
物理内存(RAM)与CPU一样都是系统中最稀少的资源,也是最有可能产生竞争的资源,应用内存与性能直接相关
由于iOS中没有交换空间作为备选资源,使得内存资源尤为重要(通常是以牺牲被的应用为代价)
Q:如何APP占用的内存?
A:获取app内存的API同样可以在Mach层找到,mach_task_basic_info结构体存储了Mach task的内存使用信息
mach_task_basic_info 结构体存储了 Mach task 的内存使用信息,其中 resident_size 就是应用使用的物理内存大小,virtual_size 是虚拟内存大小
值得注意的是task_basic_info在 Apple 已经不建议再使用,而是使用mach_task_basic_info
task_info API 根据指定的 flavor 类型返回 target_task 的信息
FPS监控
FPS 是测量用于保存、显示动态视频的信息数量,每秒钟帧数愈多,所显示的动作就会愈流畅,一般应用只要保持 FPS 在 50-60,应用就会给用户流畅的感觉,反之,用户则会感觉到卡顿
主要是基于CADisplayLink以屏幕刷新频率同步绘图的特性,尝试根据这点去实现一个可以观察屏幕当前帧数的指示器
主要原理:
1、CADisplayLink 默认每秒60次
2、将CADisplayLink add到 mainRunLoop中
3、使用CADisplayLink的timestamp属性,在CADisplayLink每次tick时,记录上一次timestamp
4、用_count记录CADisplayLink tick的执行次数
5、计算此次tick时,CADisplayLink的当前timestamp和_lastTime的差值delta
6、如果差值大于1,fps=_count/delta,计算得出FPS数
值得注意的是基于CADisplayLink实现的 FPS 在生产场景中只有指导意义,不能代表真实的 FPS,因为基于CADisplayLink实现的 FPS 无法完全检测出当前 Core Animation 的性能情况,它只能检测出当前 RunLoop的帧率
Thread(主线程卡顿监控)
当监控到应用出现卡顿,如何定位造成卡顿的原因呢?试想如果能够在发生卡顿的时候,保存应用的上下文,即卡顿发生时程序的堆栈调用和运行日志,那么就能凭借这些信息更加高效地定位到造成卡顿问题的来源
监控界面卡顿,主要是监控主线程做了哪些耗时的操作,(之前分享渲染流程时提到过卡顿),iOS中线程的事件处理依靠的是RunLoop,正常FPS值为60,如果单次RunLoop运行循环的事件超过16.66ms,就会使得FPS值低于60,如果耗时更多,就会有明显的卡顿
runloop观察者: Runloop Observer有7种状态 (CF-1151.16版本)
input sources : 传递异步事件, 通常消息来自于其他线程或程序.
timer sources : 传递同步事件, 发生在特定时间或者重复的时间间隔.
当sources到来的时候, 唤醒RunLoop, 该干嘛干嘛. 具体流程可如下所示 :
Apple源码具体实现在CF框架中,CFRunloop.c中 __CFRunLoopRun方法可查看(可自行下载看)
从运行循环中可以看出,RunLoop休眠的事件是无法衡量的,处理事件的部分主要是在kCFRunLoopBeforeSources之后到kCFRunLoopBeforeWaiting之前和kCFRunLoopAfterWaiting 之后和运行循环结束之前这两个部分
监控这两个部分的耗时,使用CFRunLoopObserverRef来监控RunLoop的状态
需要注意的是,对卡顿的判断是通过kCFRunLoopBeforeSources或者kCFRunLoopBeforeWaiting这两个状态开始后,信号量+1,这时候信号量>0,dispatch_semaphore_wait不会阻塞,返回0,进行下一个while循环,如果此时还没有进入下一个RunLoop状态,此时信号量=0,dispatch_semaphore_wait就会在这里阻塞,到了设定的超时时间,dispatch_semaphore_wait的返回值>0,这时候就会进行耗时的判断。我们可以自己设定超时时间和超过多少次算卡顿,比如:设置超过250ms