APP的性能监控包括: CPU 占用率、 内存使用情况、网络状况监控、启动时闪退、卡顿、FPS、使用时崩溃、耗电量监控、流量监控等等。
- CPU占用率
APP在运行的时候,会对应一个Mach Task,而Task下可能有多条线程同时执行任务,每个线程都是作为利用CPU的基本单位。所以我们可以通过获取当前Mach Task下,所有线程占用 CPU 的情况,来计算APP的 CPU 占用率。
一个Mach Task包含它的线程列表。内核提供了task_threads
API 调用获取指定 task 的线程列表,然后可以通过thread_info
API调用来查询指定线程的信息,在thread_act.h
中有相关定义。
task_threads
将target_task
任务中的所有线程保存在act_list数组中,act_listCnt表示线程个数:
kern_return_t task_threads
(
task_t target_task,
thread_act_array_t *act_list,
mach_msg_type_number_t *act_listCnt
);
thread_info
结构如下:
kern_return_t thread_info
(
thread_act_t target_act,
thread_flavor_t flavor, // 传入不同的宏定义获取不同的线程信息
thread_info_t thread_info_out, // 查询到的线程信息
mach_msg_type_number_t *thread_info_outCnt // 信息的大小
);
所以我们如下来获取CPU的占有率:
#import "LSLCpuUsage.h"
#import <mach/task.h>
#import <mach/vm_map.h>
#import <mach/mach_init.h>
#import <mach/thread_act.h>
#import <mach/thread_info.h>
@implementation LSLCpuUsage
+ (double)getCpuUsage {
kern_return_t kr;
thread_array_t threadList; // 保存当前Mach task的线程列表
mach_msg_type_number_t threadCount; // 保存当前Mach task的线程个数
thread_info_data_t threadInfo; // 保存单个线程的信息列表
mach_msg_type_number_t threadInfoCount; // 保存当前线程的信息列表大小
thread_basic_info_t threadBasicInfo; // 线程的基本信息
// 通过“task_threads”API调用获取指定 task 的线程列表
// mach_task_self_,表示获取当前的 Mach task
kr = task_threads(mach_task_self(), &threadList, &threadCount);
if (kr != KERN_SUCCESS) {
return -1;
}
double cpuUsage = 0;
for (int i = 0; i < threadCount; i++) {
threadInfoCount = THREAD_INFO_MAX;
// 通过“thread_info”API调用来查询指定线程的信息
// flavor参数传的是THREAD_BASIC_INFO,使用这个类型会返回线程的基本信息,
// 定义在 thread_basic_info_t 结构体,包含了用户和系统的运行时间、运行状态和调度优先级等
kr = thread_info(threadList[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount);
if (kr != KERN_SUCCESS) {
return -1;
}
threadBasicInfo = (thread_basic_info_t)threadInfo;
if (!(threadBasicInfo->flags & TH_FLAGS_IDLE)) {
cpuUsage += threadBasicInfo->cpu_usage;
}
}
// 回收内存,防止内存泄漏
vm_deallocate(mach_task_self(), (vm_offset_t)threadList, threadCount * sizeof(thread_t));
return cpuUsage / (double)TH_USAGE_SCALE * 100.0;
}
@end
- 内存监控
可以使用instruments的Leaks监测内存泄露问题
如果需要使用代码获取内存使用情况,可以通过指定flavor
参数为task_vm_info_data_t
的task info
中的phys_footprint
属性来获取。
- (double)memoryUsage {
double memoryUsageInByte = -1;
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (kernelReturn == KERN_SUCCESS) {
// 获取到的内存占用值转换成MB
memoryUsageInByte = (double)vmInfo.phys_footprint / (1024 * 1024);
NSLog(@"Memory in use (in MB): %lld", memoryUsageInByte);
} else {
NSLog(@"Error with task_info(): %s", mach_error_string(kernelReturn));
}
return memoryUsageInByte;
}
- 卡顿
现在的手机设备基本都是采用双缓存+垂直同步(即V-Sync
)屏幕显示技术。
如图所示,系统内CPU
、GPU
和显示器是协同完成显示工作的。其中CPU
负责计算显示的内容,例如视图创建、布局计算、图片解码、文本绘制等等。随后CPU
将计算好的内容提交给GPU
,由GPU
进行变换、合成、渲染。GPU
会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU
会直接将视频控制器的指针指向第二个容器(双缓存原理)。这里,GPU
会等待显示器的VSync
(即垂直同步)信号发出后,才进行新的一帧渲染和缓冲区更新(这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟)。
造成卡顿的原因如下图所示,每次扫描枪扫描到最后一行时会发出VSync
信号,每次VSync
信号发出时即表示马上需要显示下一帧画面,因此在VSync
信号发出时必须要保证CPU
完成对后一帧画面的布局计算、图片解码以及GPU
的渲染等操作,假如CPU
在进行布局计算等运算时被其它耗时操作影响了,那么就会延后GPU
渲染时间,那么如果这时候有VSync
信号发出来,就没办法显示这一帧画面,画面保持上一帧的内容,这就造成了掉帧,从而在视觉上看到了画面卡顿。
那么如何检测卡顿呢?
主要有两个方案,方案1,监测FPS,方案2,子线程监测主线程是否响应超时。
在使用FPS监控性能的实践过程中,发现 FPS 值抖动较大,造成侦测卡顿比较困难,因此可以采用主线程卡顿监控的方案,即检测主线程每次执行消息循环的时间,当这一时间大于某个阈值时,就记为发生一次卡顿。在实践中我们发现,有的卡顿连续性耗时较长,例如打开新页面时的卡顿;而有的卡顿连续性耗时相对较短但频次较快,例如列表滑动时的卡顿。因此,我们采用了“N次卡顿超过阈值T”的判定策略,即一个时间段内卡顿的次数累计大于N时才触发采集和上报:例如卡顿阈值T=2000ms、卡顿次数N=1,可以判定为单次耗时较长的卡顿;而卡顿阈值T=300ms、卡顿次数N=5,可以判定为频次较快的卡顿。
#define lsl_SEMAPHORE_SUCCESS 0
static BOOL lsl_is_monitoring = NO;
static dispatch_semaphore_t lsl_semaphore;
static NSTimeInterval lsl_time_out_interval = 0.05;
@implementation LSLAppFluencyMonitor
static inline dispatch_queue_t __lsl_fluecy_monitor_queue() {
static dispatch_queue_t lsl_fluecy_monitor_queue;
static dispatch_once_t once;
dispatch_once(&once, ^{
lsl_fluecy_monitor_queue = dispatch_queue_create("com.dream.lsl_monitor_queue", NULL);
});
return lsl_fluecy_monitor_queue;
}
static inline void __lsl_monitor_init() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lsl_semaphore = dispatch_semaphore_create(0);
});
}
#pragma mark - Public
+ (instancetype)monitor {
return [LSLAppFluencyMonitor new];
}
- (void)startMonitoring {
if (lsl_is_monitoring) { return; }
lsl_is_monitoring = YES;
__lsl_monitor_init();
dispatch_async(__lsl_fluecy_monitor_queue(), ^{
while (lsl_is_monitoring) {
__block BOOL timeOut = YES;
dispatch_async(dispatch_get_main_queue(), ^{
timeOut = NO;
dispatch_semaphore_signal(lsl_semaphore);
});
[NSThread sleepForTimeInterval: lsl_time_out_interval];
if (timeOut) {
[LSLBacktraceLogger lsl_logMain]; // 打印主线程调用栈
// [LSLBacktraceLogger lsl_logCurrent]; // 打印当前线程的调用栈
// [LSLBacktraceLogger lsl_logAllThread]; // 打印所有线程的调用栈
}
dispatch_wait(lsl_semaphore, DISPATCH_TIME_FOREVER);
}
});
}
- (void)stopMonitoring {
if (!lsl_is_monitoring) { return; }
lsl_is_monitoring = NO;
}
@end
其它性能监控内容可以参考iOS开发--APP性能检测方案汇总(一)以及美团的技术文档