乐趣区

iOS App卡顿监控(Freezing/Lag)

iOS App 卡顿监控 (Freezing/Lag)
笔记 (写在前面):
关于应用的性能监控,需要从多方面进行综合考虑,此处仅从其中一个方面,进行学习研究。

如何判断主线程卡顿:
监测 NSRunLoop 耗时情况。
NSRunLoop 的调用主要在 kCFRunLoopBeforeSources 和 kCFRunLoopBeforeWaiting 之间,以及 kCFRunLoopAfterWaiting 之后。因此,若是发现这个两个时间内耗时过长,就可以判定此时主线程出现卡顿情况。

一、监控 NSRunLoop 状态变化
使用 CFRunLoopObserverRef,实时获得这些状态值的变化,如下:
/// RunLoop 状态观察回调
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
<#MyClass#> *object = (__bridge <#MyClass#>*)info;
// 记录状态值
object->activity = activity;
}
/// 注册 RunLoop 状态观察
– (void)registerRunLoopObserver {
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
二、RunLoop 耗时计算
另外开启一个线程,实时计算两个状态区域之间的耗时,是否达到阈值。dispatch_semaphore_t 让子线程更及时地获知主线程 NSRunLoop 状态变化
卡顿覆盖范围:多次连续小卡顿、单次长时间卡顿

添加计算逻辑,如下:
/// RunLoop 状态观察回调
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
<#MyClass#> *object = (__bridge <#MyClass#>*)info;
// 记录状态值
object->activity = activity;

// 发送信号
dispatch_semaphore_t semaphore = object->semaphore;
dispatch_semaphore_signal(semaphore);
}
/// 注册 RunLoop 状态观察,并计算是否卡顿
– (void) registerRunLoopObserver {
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

// 创建信号
semaphore = dispatch_semaphore_create(0);

// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) {
// 假定连续 5 次超时 50ms 认为卡顿 (当然也包含了单次超时 250ms)
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0) {
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting) {
if (++timeoutCount < 5) {
continue;
}
// 发现卡顿
NSLog(@” 卡、卡、卡、顿、顿、了 ”);
}
}
timeoutCount = 0;
}
});
}
三、记录卡顿的函数调用
目击卡顿现场,记录此时的调用函数信息,作为卡顿证据。
此处,使用第三方 Crash 收集组件 PLCrashReporter,它不仅可以收集 Crash 信息,也可用于实时获取各线程的调用堆栈,使用示例如下:
PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
NSData *data = [crashReporter generateLiveReport];
PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
withTextFormat:PLCrashReportTextFormatiOS];
NSLog(@”————\n%@\n————“, report);
特别注意:

PLCrashReporter 虽然能提供较为准确的堆栈信息,用于定位问题,特别是使用符号化策略 PLCrashReporterSymbolicationStrategyAll 时,能够对堆栈信息进行符号化,但会消耗大量资源,需要占用较多时间,导致卡死现象(自测时,耗时超过 7s,层多次到 10s 以上)。不使用符号化策略 PLCrashReporterSymbolicationStrategyNone,测试时,平均耗时也接近 3s。
因此,加入该信息采集,需要特别注意,建议仅在开发调试阶段使用。
为了投入线上使用,还需要再想想如何解决该问题。

四、上报服务器
检测到卡顿,获取到调用堆栈信息,客户端再根据实际情况进行一定程度的过滤处理,将有价值的信息上报服务器。
后续对服务器收集到的数据进行分析,定位需要优化的功能逻辑。

退出移动版