微信Matrix 卡顿监控原理梳理与图解

如果你想建立公司自己的APM监控平台,卡顿检测这项来说,微信Matrix是一个很好的参考;如果想进一步监控卡顿的实际时长,当卡顿时长达到8秒时升级为卡死级别(ANR)上报,可以在这个基础上增加判断逻辑。

下文是根据源码梳理的图解实现原理

WCBlockMonitorMgr 是 微信Matrix 中用于监控 iOS 主线程卡顿(ANR / Hang)的核心模块。其设计思路是:通过 RunLoop Observer 感知主线程状态变化,再通过独立的后台线程周期性检查主线程是否"卡死"超过阈值,若卡顿则收集堆栈、CPU 等信息并生成 dump 文件上报。同时它还集成了 CPU 高负载检测、耗电堆栈采集、内存监控等功能。

下面从五个方面详细讲解实现原理,并辅以图解。


一、核心监控流程概览

text 复制代码
+----------------+       +-------------------+       +-------------------+
| 主线程 RunLoop | --->  | RunLoop Observer   | ---> | 全局状态变量更新   |
| (UI/事件处理)   |       | (开始/结束回调)    |       | g_bRun, g_tvRun   |
+----------------+       +-------------------+       +-------------------+
                                                              |
                                                              v
+----------------+       +-------------------+       +-------------------+
| 监控线程        | <--- | 定期检查             | ---> | 计算卡顿时长 diff  |
| (独立 thread)  |       | (每轮 50ms 堆栈采样)|       | diff > 阈值?      |
+----------------+       +-------------------+       +-------------------+
                                                              |
                                    +-------------------------+
                                    |                         |
                                    v                         v
                          +-------------------+    +-------------------+
                          | 卡顿触发           |    | 无卡顿,继续采样   |
                          | 收集堆栈/CPU/快照  |    | 退火算法调整间隔   |
                          | 生成 dump 文件     |    +-------------------+
                          +-------------------+

关键参数 (单位均为微秒,但代码中常用 BM_MicroFormat_Second 等宏):

  • g_RunLoopTimeOut:卡顿判定阈值,默认 2 秒(可动态调整)。
  • g_CheckPeriodTime:无论是否卡顿,监控线程的单次检查周期,通常为 g_RunLoopTimeOut / 2
  • g_PerStackInterval:连续采样堆栈的间隔,固定 50ms(代码中 BM_MicroFormat_FrameMillSecond)。

二、RunLoop 观测与状态记录

RunLoop 在每次循环的不同阶段(Entry、BeforeTimers、BeforeSources、AfterWaiting、BeforeWaiting、Exit)会触发 Observer 回调。WCBlockMonitorMgr 添加了两个 Observer

  • beginObserver (优先级 LONG_MIN):在 RunLoop 开始处理 事件时(Entry、BeforeTimers、BeforeSources、AfterWaiting)将 g_bRun 置为 YES,并记录开始时间 g_tvRun
  • endObserver (优先级 LONG_MAX):在 RunLoop 即将休眠 (BeforeWaiting)或 退出 (Exit)时将 g_bRun 置为 NO

另外还针对启动阶段(UIInitializationRunLoopMode)添加了单独的 Observer,用于处理启动卡顿。

状态图:

text 复制代码
    [RunLoop 启动] 
          │
          ▼
   kCFRunLoopEntry  ──▶ g_bRun = YES; 记录开始时间
          │
          ▼
   BeforeTimers / BeforeSources / AfterWaiting
          │
          ▼
    (主线程忙碌处理事件)
          │
          ▼
   BeforeWaiting (即将休眠) ──▶ g_bRun = NO; 可选敏感检测
          │
          ▼
    (休眠等待事件)
          │
          ▼
   唤醒后回到 AfterWaiting,重复

监控线程读取 g_bRung_tvRun,若当前 g_bRun == YES 且距离 g_tvRun 的时长超过 g_RunLoopTimeOut,则认为主线程发生了卡顿。

注意:g_bRun 在 Observer 中更新,是跨线程共享的原子变量(实际未加锁,但因访问简单且时序不苛刻,可接受)。


三、监控线程的工作机制

监控线程在 threadProc 中无限循环(内部有休眠),每次循环执行以下步骤:

  1. 检查卡顿 (调用 check 方法):

    • 计算当前时间与 g_tvRun 的差值 diff
    • g_bRun == YESdiff > g_RunLoopTimeOut,则判定卡顿。
    • 同时还会检查 CPU 使用率是否超过阈值(g_CPUUsagePercent,默认 1000 即忽略,实际可配置)。
    • 返回对应的 EDumpType(如 MainThreadBlockBackgroundMainThreadBlockCPUBlock 等)。
  2. 卡顿处理

    • 调用 needFilter 进行退火过滤:对比当前卡顿的堆栈与上次卡顿堆栈是否相同,若相同则放大检查间隔(退火算法),避免重复上报相同的堆栈。
    • 若未过滤,则收集主线程堆栈(来自循环队列 WCMainThreadHandler 中保存的多次采样结果)或 CPU 高负载堆栈。
    • 调用 dumpFileWithType 生成 dump 文件(同时可选择挂起所有线程并生成快照)。
    • 通过 delegate 回调通知上层。
  3. 记录当前堆栈recordCurrentStack):

    • 无论是否卡顿,每一轮都会以 g_CheckPeriodTime 为周期,内部按 g_PerStackInterval 多次获取主线程调用栈,存入 WCMainThreadHandler 的循环队列中。
    • 这样在卡顿发生时,可以拿到卡顿期间的多组堆栈,而非仅某一瞬间的堆栈。
  4. 重置状态 :若无卡顿,调用 resetStatus 恢复监控参数。

监控线程时序图:

text 复制代码
监控线程                    主线程 (RunLoop)
   │                            │
   ├─ 等待 g_CheckPeriodTime ──→│ (忙碌)
   │                            │
   ├─ 检查卡顿 ←────────────────┤ (g_bRun = YES; )
   │   (diff > 阈值)             │
   ├─ 卡顿触发                   │
   │   ├─ 收集堆栈 (队列中已有)   │
   │   ├─ 生成 dump              │
   │   └─ 回调                   │
   │                            │
   ├─ 继续下一轮监控             │
   │                            │

四、堆栈采集与退火算法

1. 堆栈采集

  • 通过 WCGetMainThreadUtil 获取主线程调用栈(内部使用 kscrashstacktracethread_get_state)。
  • 也是在上述监控线程中进行,每 g_PerStackInterval(50ms)采集一次,每次最多保存 g_StackMaxCount 个地址。采集后休眠。
  • 这些地址被存入 WCMainThreadHandler 的循环队列,队列长度由 g_MainThreadCount = g_CheckPeriodTime / g_PerStackInterval 决定(例如 500ms / 50ms = 10 个槽位)。

2. 退火算法(needFilter

  • 将当前卡顿时的堆栈与上一次卡顿的堆栈逐帧比较。
  • 若完全相同(表明可能是同样的代码循环卡顿),则:
    • 将检查间隔 m_nIntervalTime 扩大为 m_nLastTimeInterval + m_nIntervalTime(退火)。
    • 返回 EFilterType_Annealing,跳过本次上报。
  • 若不同,则恢复 m_nIntervalTime = g_CheckPeriodTime,并更新保存的堆栈副本。
  • 同时还会检查今日已上报次数是否超过限额(getDumpDailyLimit)。

算法示意图:

text 复制代码
首次卡顿 Stack A
   │
   ▼
第二次卡顿 Stack A' ──→ 比对相同 ──→ 退火: 等待间隔加倍 (例如 1s → 2s)
   │                                    │
   │                                    ▼
   │                               再次卡顿仍相同 → 间隔继续扩大
   │                                    │
   │                                    ▼
   │                               跳过上报,避免日志泛滥
   │
   ▼
第三次卡顿 Stack B (不同) ──→ 重置间隔,正常上报

五、辅助功能与扩展

  • CPU 监控 :通过 WCCPUHandler 累计平均 CPU 使用率,若持续高则触发 CPUBlock 类型 dump。
  • 耗电堆栈WCPowerConsumeStackCollector 在 CPU 长时间高于阈值时,采集消耗 CPU 的热点堆栈树并异步保存。
  • 内存监控:定期打印 footprint,超过阈值(如 400MB)回调 delegate。
  • RunLoop 敏感检测 :当 g_bSensitiveRunloopHangDetection 开启时,在每次 BeforeWaiting 前立即检查主线程当前已执行时长,若超过 250ms 则异步回调(类似苹果 HangTracer)。
  • 动态阈值调整 :提供 lowerRunloopThreshold / recoverRunloopThreshold 方法,可动态降低卡顿阈值(如进入页面后降低到 400ms),退出后恢复。

六、完整流程图(文字版)

text 复制代码
+-----------------------------+
|  App 启动                    |
+-----------------------------+
              │
              ▼
+-----------------------------+
|  添加 RunLoop Observers      |
|  (begin/end, 两种 mode)      |
+-----------------------------+
              │
              ▼
+-----------------------------+
|  启动监控线程 (threadProc)    |
+-----------------------------+
              │
     ┌────────┴────────┐
     │                 │
     ▼                 ▼
+------------+   +------------+
| 主线程     |   | 监控线程    |
| RunLoop    |   | 循环:       |
| 触发回调   |   | 1. 检查卡顿 |
| 更新       |   | 2. 若卡顿   |
| g_bRun/    |   |   过滤重复  |
| g_tvRun    |   |   生成dump  |
+------------+   | 3. 采样堆栈 |
                 | 4. 休眠     |
                 +------------+
                       │
                       ▼
                  +----------+
                  | 卡顿上报  |
                  | (delegate)|
                  +----------+

状态转换细节(卡顿检测部分):

text 复制代码
       [RunLoop 开始处理]
              │
              ▼
      (g_bRun=YES, 记录开始)
              │
              │ 主线程长时间不进入休眠
              │ (例如死循环、大量IO)
              ▼
      监控线程计算 diff > 阈值
              │
              ├─ 是 ──→ 判定卡顿
              │         │
              │         ├─ 堆栈是否重复?
              │         │    ├─ 是 → 退火,不上报
              │         │    └─ 否 → 生成 dump
              │         │
              │         └─ 回调通知
              │
              └─ 否 ──→ 继续监控

七、关键代码片段注释(对应原理)

原理部分 代码位置(函数/行)
添加 RunLoop Observer addRunLoopObserver,使用 CFRunLoopAddObserver
状态变量更新 myRunLoopBeginCallback / myRunLoopEndCallback
监控线程循环 threadProc 中的 while(YES)
卡顿判定 check 方法中计算 diff > g_RunLoopTimeOut
堆栈采集 recordCurrentStack 内循环调用 WCGetMainThreadUtil
退火过滤 needFilter 方法
动态阈值调整 setRunloopThreshold:

通过以上机制,WCBlockMonitorMgr 能够在不显著影响性能的前提下,高效准确地捕获主线程卡顿,并提供丰富的辅助信息用于定位问题。

相关推荐
2501_916007477 小时前
iOS开发中抓取HTTPS请求的完整解决方法与步骤详解
android·网络协议·ios·小程序·https·uni-app·iphone
ZZH_AI项目交付10 小时前
我把 AI 最容易改坏真实 App 的地方,整理成了 skills
人工智能·ios·app
00后程序员张11 小时前
Windows 下怎么生成 AppStoreInfo.plist?不依赖 Xcode 的方法
ide·macos·ios·小程序·uni-app·iphone·xcode
原鸣清11 小时前
iOS 自定义 Markdown 渲染实践:从成品库到可魔改 Demo
ios
Daniel_Coder11 小时前
iOS Widget 开发-18:Widget 的 SwiftUI 视图适配与设计
ios·swiftui·swift·widget·widgetcenter
Daniel_Coder12 小时前
iOS Widget 开发-17:Widget 错误处理与空状态设计
ios·swift·widget·widgetcenter
wjm04100612 小时前
简单谈谈ios开发中的UI
开发语言·ios·swift
恋猫de小郭13 小时前
Flutter 3.44 发布啦,超级大版本更新!!!
android·flutter·ios
天天开发14 小时前
Flutter开发者该掌握的iOS隐私审核政策
flutter·ios·cocoa