质量监控-卡顿检测

原文连接html

不论是应用秒变幻灯片,仍是启动太久被杀,基本都是开发者必经的体验。就像没人但愿堵车同样,卡顿永远是不受用户欢迎的,因此如何发现卡顿是开发者须要直面的难题。虽然致使卡顿的缘由有不少,但卡顿的表现老是大同小异。若是把卡顿当作病症看待,二者分别对应所谓的本与标。要检测卡顿,不管是标或本均可如下手,但都须要深刻的学习算法

instruments与性能

在开发阶段,使用内置的性能工具instruments来检测性能问题是最佳的选择。与应用运行性能关联最紧密的两个硬件CPUGPU,前者用于执行程序指令,针对代码的处理逻辑;后者用于大量计算,针对图像信息的渲染。正常状况下,CPU会周期性的提交要渲染的图像信息给GPU处理,保证视图的更新。一旦其中之一响应不过来,就会表现为卡顿。所以多数状况下用到的工具是检测GPU负载的Core Animation,以及检测CPU处理效率的Time Profilermarkdown

因为CPU提交图像信息是在主线程执行的,会影响到CPU性能的诱因包括如下:网络

  1. 发生在主线程的I/O任务
  2. 过多的线程抢占CPU资源
  3. 温度太高致使的CPU降频

而影响GPU的因素较为客观,难以针对作代码上的优化,包括:async

  1. 显存频率
  2. 渲染算法
  3. 大计算量

本文旨在介绍如何去检测卡顿,而非如何解决卡顿,所以若是对上面列出的诱因有兴趣的读者能够自行阅读相关文章书籍函数

卡顿检测

检测的方案根据线程是否相关分为两大类:工具

  • 执行耗时任务会致使CPU短期没法响应其余任务,检测任务耗时来判断是否可能致使卡顿
  • 因为卡顿直接表现为操做无响应,界面动画迟缓,检测主线程是否能响应任务来判断是否卡顿

与主线程相关的检测方案包括:oop

  1. fps
  2. ping
  3. runloop

与主线程不相关的检测包括:性能

  1. stack backtrace
  2. msgSend observe

衡量指标

不一样方案的检测原理和实现机制都不一样,为了更好的选择所需的方案,须要创建一套衡量指标来对方案进行对比,我的总结的衡量指标包括四项:学习

  • 卡顿反馈

    卡顿发生时,检测方案是否能及时、直观的反馈出本次卡顿

  • 采集精度

    卡顿发生时,检测方案可否采集到充足的信息来作定位追溯

  • 性能损耗

    维持检测所需的CPU占用、内存使用是否会引入额外的问题

  • 实现成本

    检测方案是否易于实现,代码的维护成本与稳定性等

fps

一般状况下,屏幕会保持60hz/s的刷新速度,每次刷新时会发出一个屏幕刷新信号,CADisplayLink容许咱们注册一个与刷新信号同步的回调处理。能够经过屏幕刷新机制来展现fps值:

- (void)startFpsMonitoring {
    WeakProxy *proxy = [WeakProxy proxyWithClient: self];
    self.fpsDisplay = [CADisplayLink displayLinkWithTarget: proxy selector: @selector(displayFps:)];
    [self.fpsDisplay addToRunLoop: [NSRunLoop mainRunLoop] forMode: NSRunLoopCommonModes];
}

- (void)displayFps: (CADisplayLink *)fpsDisplay {
    _count++;
    CFAbsoluteTime threshold = CFAbsoluteTimeGetCurrent() - _lastUpadateTime;
    if (threshold >= 1.0) {
        [FPSDisplayer updateFps: (_count / threshold)];
        _lastUpadateTime = CFAbsoluteTimeGetCurrent();
    }
}
复制代码
指标
卡顿反馈 卡顿发生时,fps会有明显下滑。但转场动画等特殊场景也存在下滑状况。高
采集精度 回调老是须要cpu空闲才能处理,没法及时采集调用栈信息。低
性能损耗 监听屏幕刷新会频繁唤醒runloop,闲置状态下有必定的损耗。中低
实现成本 单纯的采用CADisplayLink实现。低
结论 更适用于开发阶段,线上可做为辅助手段

ping

ping是一种经常使用的网络测试工具,用来测试数据包是否能到达ip地址。在卡顿发生的时候,主线程会出现短期内无响应这一表现,基于ping的思路从子线程尝试通讯主线程来获取主线程的卡顿延时:

@interface PingThread : NSThread
......
@end

@implementation PingThread

- (void)main {
    [self pingMainThread];
}

- (void)pingMainThread {
    while (!self.cancelled) {
        @autoreleasepool {
            dispatch_async(dispatch_get_main_queue(), ^{
                [_lock unlock];
            });
            
            CFAbsoluteTime pingTime = CFAbsoluteTimeGetCurrent();
            NSArray *callSymbols = [StackBacktrace backtraceMainThread];
            [_lock lock];
            if (CFAbsoluteTimeGetCurrent() - pingTime >= _threshold) {
                ......
            }
            [NSThread sleepForTimeInterval: _interval];
        }
    }
}

@end
复制代码
指标
卡顿反馈 主线程出现堵塞直到空闲期间都没法回包,但在ping之间的卡顿存在漏查状况。中高
采集精度 子线程在ping前能获取主线程准确的调用栈信息。中高
性能损耗 须要常驻线程和采集调用栈。中
实现成本 须要维护一个常驻线程,以及对象的内存控制。中低
结论 监控能力、性能损耗和ping频率都成正比,监控效果强

runloop

做为和主线程相关的最后一个方案,基于runloop的检测和fps的方案很是类似,都须要依赖于主线程的runloop。因为runloop会调起同步屏幕刷新的callback,若是loop的间隔大于16.67msfps天然达不到60hz。而在一个loop当中存在多个阶段,能够监控每个阶段停留了多长时间:

- (void)startRunLoopMonitoring {
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        if (CFAbsoluteTimeGetCurrent() - _lastActivityTime >= _threshold) {
            ......
            _lastActivityTime = CFAbsoluteTimeGetCurrent();
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
复制代码
指标
卡顿反馈 runloop的不一样阶段把时间分片,若是某个时间片太长,基本认定发生了卡顿。此外应用闲置状态常驻beforeWaiting阶段,此阶段存在误报可能。中
采集精度 fps相似的,依附于主线程callback的方案缺乏准确采集调用栈的时机,但优于fps检测方案。中低
性能损耗 此方案不会频繁唤醒runloop,相较于fps性能更佳。低
实现成本 须要注册runloop observer。中低
结论 综合性能优于fps,但反馈表现不足,只适合做为辅助工具使用

stack backtrace

代码质量不够好的方法可能会在一段时间内持续占用CPU的资源,换句话说在一段时间内,调用栈老是停留在执行某个地址指令的状态。因为函数调用会发生入栈行为,若是比对两次调用栈的符号信息,前者是后者的符号子集时,能够认为出现了卡顿恶鬼

@interface StackBacktrace : NSThread
......
@end

@implementation StackBacktrace

- (void)main {
    [self backtraceStack];
}

- (void)backtraceStack {
    while (!self.cancelled) {
        @autoreleasepool {
            NSSet *curSymbols = [NSSet setWithArray: [StackBacktrace backtraceMainThread]];
            if ([_saveSymbols isSubsetOfSet: curSymbols]) {
                ......
            }
            _saveSymbols = curSymbols;
            [NSThread sleepForTimeInterval: _interval];
        }
    }
}

@end
复制代码
指标
卡顿反馈 因为符号地址的惟一性,调用栈比对的准确性高。但须要排除闲置状态下的调用栈信息。高
采集精度 直接经过调用栈符号信息比对能够准确的获取调用栈信息。高
性能损耗 须要频繁获取调用栈,须要考虑延后符号化的时机减小损耗。中高
实现成本 须要维护常驻线程和调用栈追溯算法。中高
结论 准确率很高的工具,适用面广

msgSend observe

OC方法的调用最终转换成msgSend的调用执行,经过在函数先后插入自定义的函数调用,维护一个函数栈结构能够获取每个OC方法的调用耗时,以此进行性能分析与优化:

#define save() \
__asm volatile ( \
    "stp x8, x9, [sp, #-16]!\n" \
    "stp x6, x7, [sp, #-16]!\n" \
    "stp x4, x5, [sp, #-16]!\n" \
    "stp x2, x3, [sp, #-16]!\n" \
    "stp x0, x1, [sp, #-16]!\n");

#define resume() \
__asm volatile ( \
    "ldp x0, x1, [sp], #16\n" \
    "ldp x2, x3, [sp], #16\n" \
    "ldp x4, x5, [sp], #16\n" \
    "ldp x6, x7, [sp], #16\n" \
    "ldp x8, x9, [sp], #16\n" );
    
#define call(b, value) \
    __asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
    __asm volatile ("mov x12, %0\n" :: "r"(value)); \
    __asm volatile ("ldp x8, x9, [sp], #16\n"); \
    __asm volatile (#b " x12\n");


__attribute__((__naked__)) static void hook_Objc_msgSend() {

    save()
    __asm volatile ("mov x2, lr\n");
    __asm volatile ("mov x3, x4\n");
    
    call(blr, &push_msgSend)
    resume()
    call(blr, orig_objc_msgSend)
    
    save()
    call(blr, &pop_msgSend)
    
    __asm volatile ("mov lr, x0\n");
    resume()
    __asm volatile ("ret\n");
}
复制代码
指标
卡顿反馈
采集精度
性能损耗 拦截后调用频次很是高,启动阶段可达10w次以上调用。高
实现成本 须要维护方法栈和优化拦截算法。高
结论 准确率很高的工具,但不适用于Swift代码

总结

fps ping runloop stack backtrace msgSend observe
卡顿反馈 中高
采集精度 中高 中低
性能损耗 中低 中高
实现成本 中低 中低 中高

关注个人公众号获取更新信息
相关文章
相关标签/搜索