matrix 是腾讯微信团队开源的一套移动端性能监控与分析框架,核心目标是帮助开发者定位、解决移动端(iOS/Android)应用的性能问题,是微信内部大规模验证过的成熟工具,本文通过阅读源码,详细介绍了针对卡顿日志获取的核心原理。
Matrix 通过周期性采集主线程堆栈并保存在循环数组中,在检测到卡顿时,使用 Point Stack 算法找出最有可能导致卡顿的堆栈。
核心思想
时间流逝
↓
每 50ms 采集一次主线程堆栈
↓
保存到循环数组(固定大小,如 20 个)
↓
检测到卡顿时
↓
分析循环数组,找出 Point Stack(最可能导致卡顿的堆栈)
↓
生成卡顿报告
设计目标
| 目标 | 实现方式 |
|---|---|
| 及时性 | 每 50ms 采集一次,不错过卡顿过程 |
| 完整性 | 保存一个周期内的所有堆栈(通常 20 个) |
| 准确性 | 通过算法找出真正导致卡顿的堆栈 |
| 高效性 | 固定大小循环数组,避免内存膨胀 |
| 低开销 | CPU 占用 < 3%,不影响用户体验 |
核心时间参数
三个关键参数
// 1. RunLoop 超时阈值(卡顿判定阈值)
static useconds_t g_RunLoopTimeOut = 2000000; // 2000ms = 2秒
// 作用:超过此时间判定为卡顿
// 2. 检查周期(单次采集周期)
static useconds_t g_CheckPeriodTime = 1000000; // 1000ms = 1秒
// 作用:一轮堆栈采集的总时间,通常为超时阈值的一半
// 3. 堆栈采集间隔
static useconds_t g_PerStackInterval = 50000; // 50ms
// 作用:每次堆栈采集之间的时间间隔
参数关系
┌────────────────────────────────────────────────┐
│ g_RunLoopTimeOut (2秒) - 卡顿判定阈值 │
└────────────────────────────────────────────────┘
│
├─ 一半
↓
┌────────────────────────────────────────────────┐
│ g_CheckPeriodTime (1秒) - 检查周期 │
└────────────────────────────────────────────────┘
│
├─ 除以
↓
┌────────────────────────────────────────────────┐
│ g_PerStackInterval (50ms) - 堆栈间隔 │
└────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────┐
│ g_MainThreadCount = 1000ms / 50ms = 20 │
│ (循环数组大小 = 一个周期内采集的堆栈数量) │
└────────────────────────────────────────────────┘
时间轴示意
时间线(以2秒卡顿为例):
T=0ms 开始监控
|
T=50ms 采集第1个堆栈 ← S0
|
T=100ms 采集第2个堆栈 ← S1
|
T=150ms 采集第3个堆栈 ← S2
|
...
|
T=950ms 采集第19个堆栈 ← S18
|
T=1000ms 采集第20个堆栈 ← S19 ← 完成一轮采集
| ↓
| 检查是否卡顿(检查RunLoop执行时间)
| 如果未卡顿,进入下一轮采集
|
T=1050ms 采集第21个堆栈 ← S20(覆盖S0)
|
...
|
T=2000ms 检查发现 RunLoop 执行超过 2秒
| ↓
| 触发卡顿检测
| ↓
| 分析循环数组中的 20 个堆栈
| ↓
| 找出 Point Stack(最可能导致卡顿的堆栈)
| ↓
| 生成卡顿报告
堆栈获取流程
整体流程图
┌──────────────────────────────────────────┐
│ 监控线程主循环 │
│ while (YES) { │
│ check(); // 检测卡顿 │
│ recordCurrentStack(); // 采集堆栈 │
│ } │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ recordCurrentStack() 方法 │
│ │
│ 外层循环:遍历检查周期 │
│ nTotalCnt = m_nIntervalTime / │
│ g_CheckPeriodTime │
│ 通常 = 1000ms / 1000ms = 1次 │
│ │
│ 内层循环:在一个周期内多次采集 │
│ intervalCount = g_CheckPeriodTime / │
│ g_PerStackInterval │
│ 通常 = 1000ms / 50ms = 20次 │
│ │
│ 每次循环: │
│ 1. usleep(50ms) // 等待 │
│ 2. 获取主线程堆栈 │
│ 3. 添加到循环数组 │
└──────────────────────────────────────────┘
详细步骤
步骤 1:初始化
// 在 WCBlockMonitorMgr 的 start 方法中
- (void)start {
// 计算循环数组大小
g_MainThreadCount = g_CheckPeriodTime / g_PerStackInterval;
// 例如:1000ms / 50ms = 20
// 创建主线程堆栈处理器
m_pointMainThreadHandler = [[WCMainThreadHandler alloc]
initWithCycleArrayCount:g_MainThreadCount];
// g_MainThreadCount = 20
// 意味着循环数组可以保存 20 个堆栈
}
步骤 2:周期性采集
- (void)recordCurrentStack {
// ================================================================
// 外层循环:决定执行几个检查周期
// ================================================================
// 正常情况:m_nIntervalTime = 1000ms
// 退火情况:m_nIntervalTime = 2000ms, 3000ms, 5000ms...
unsigned long nTotalCnt = m_nIntervalTime / g_CheckPeriodTime;
for (int nCnt = 0; nCnt < nTotalCnt && !m_bStop; nCnt++) {
// 记录本轮开始时间(用于检测系统挂起)
gettimeofday(&m_recordStackTime, NULL);
if (g_MainThreadHandle) {
// ========================================================
// 内层循环:在一个检查周期内多次采集
// ========================================================
// intervalCount = 1000ms / 50ms = 20
int intervalCount = g_CheckPeriodTime / g_PerStackInterval;
for (int index = 0; index < intervalCount && !m_bStop; index++) {
// 1️⃣ 等待 50ms
usleep(g_PerStackInterval); // 50000 微秒 = 50ms
// 2️⃣ 分配内存
size_t stackBytes = sizeof(uintptr_t) * g_StackMaxCount;
uintptr_t *stackArray = (uintptr_t *)malloc(stackBytes);
if (stackArray == NULL) {
continue; // 内存分配失败,跳过本次
}
// 3️⃣ 初始化
__block size_t nSum = 0;
memset(stackArray, 0, stackBytes);
// 4️⃣ 获取主线程堆栈
[WCGetMainThreadUtil
getCurrentMainThreadStack:^(NSUInteger pc) {
stackArray[nSum] = (uintptr_t)pc; // 保存程序计数器
nSum++;
}
withMaxEntries:g_StackMaxCount // 最大100个栈帧
withThreadCount:g_CurrentThreadCount];
// 5️⃣ 添加到循环数组
[m_pointMainThreadHandler addThreadStack:stackArray
andStackCount:nSum];
// 注意:stackArray 的所有权转移给 m_pointMainThreadHandler
}
}
// ============================================================
// 检测是否被系统挂起
// ============================================================
struct timeval tvCur;
gettimeofday(&tvCur, NULL);
unsigned long long diff = [WCBlockMonitorMgr diffTime:&m_recordStackTime
endTime:&tvCur];
if (diff > DETECTION_THREAD_JUDGE_SUSPEND_THRESHOLD) {
// 实际消耗时间 > 10秒,说明被挂起了
gettimeofday(&g_tvRun, NULL); // 更新时间,避免误报
MatrixInfo(@"挂起后运行,差值 %llu", diff);
return;
}
}
}
步骤 3:获取主线程堆栈(底层实现)
// WCGetMainThreadUtil 内部使用 backtrace
+ (void)getCurrentMainThreadStack:(StackCallback)callback
withMaxEntries:(size_t)maxEntries
withThreadCount:(NSUInteger)threadCount {
// 1. 获取主线程
thread_t mainThread = pthread_mach_thread_np(pthread_main_thread_np());
// 2. 暂停主线程(非常短暂,微秒级)
thread_suspend(mainThread);
// 3. 获取线程状态
_STRUCT_MCONTEXT machineContext;
mach_msg_type_number_t state_count = THREAD_STATE_COUNT;
kern_return_t kr = thread_get_state(mainThread,
THREAD_STATE,
(thread_state_t)&machineContext.__ss,
&state_count);
// 4. 回溯堆栈
if (kr == KERN_SUCCESS) {
uintptr_t backtraceBuffer[maxEntries];
size_t backtraceLength = ksbt_backtraceLength(&machineContext);
// 遍历堆栈帧
for (size_t i = 0; i < backtraceLength && i < maxEntries; i++) {
uintptr_t pc = ksbt_framePointer(&machineContext, i);
callback(pc); // 回调传递每个栈帧的地址
}
}
// 5. 恢复主线程
thread_resume(mainThread);
}
循环数组存储机制
数据结构设计
@interface WCMainThreadHandler {
// ================================================================
// 循环数组配置
// ================================================================
int m_cycleArrayCount; // 数组大小,例如 20
// ================================================================
// 循环数组(核心存储结构)
// ================================================================
uintptr_t **m_mainThreadStackCycleArray; // 二维数组
// 第一维:堆栈索引 [0, 19]
// 第二维:堆栈地址数组 uintptr_t[]
size_t *m_mainThreadStackCount; // 每个堆栈的深度
// 例如:[50, 48, 52, ..., 45]
uint64_t m_tailPoint; // 尾指针,指向下一个写入位置
// ================================================================
// 分析数据(用于 Point Stack 算法)
// ================================================================
size_t *m_topStackAddressRepeatArray; // 栈顶地址连续重复次数
// 例如:[0, 1, 2, 0, 1, 0, ...]
int *m_mainThreadStackRepeatCountArray; // Point Stack 地址总重复次数
// 动态分配,在找到 Point Stack 后创建
}
循环数组可视化
初始化状态(m_cycleArrayCount = 5):
索引: 0 1 2 3 4
┌─────┬─────┬─────┬─────┬─────┐
数组: │NULL │NULL │NULL │NULL │NULL │
└─────┴─────┴─────┴─────┴─────┘
↑
m_tailPoint = 0
添加第 1 个堆栈(S0):
索引: 0 1 2 3 4
┌─────┬─────┬─────┬─────┬─────┐
数组: │ S0 │NULL │NULL │NULL │NULL │
└─────┴─────┴─────┴─────┴─────┘
↑
m_tailPoint = 1
添加第 2-5 个堆栈:
索引: 0 1 2 3 4
┌─────┬─────┬─────┬─────┬─────┐
数组: │ S0 │ S1 │ S2 │ S3 │ S4 │
└─────┴─────┴─────┴─────┴─────┘
↑
m_tailPoint = 0 (回绕)
添加第 6 个堆栈(S5,覆盖 S0):
索引: 0 1 2 3 4
┌─────┬─────┬─────┬─────┬─────┐
数组: │ S5 │ S1 │ S2 │ S3 │ S4 │
└─────┴─────┴─────┴─────┴─────┘
↑
m_tailPoint = 1
时间顺序:S1 → S2 → S3 → S4 → S5(最新)
添加堆栈的实现
- (void)addThreadStack:(uintptr_t *)stackArray
andStackCount:(size_t)stackCount {
if (stackArray == NULL) {
return;
}
pthread_mutex_lock(&m_threadLock);
// ================================================================
// 1. 将堆栈写入循环数组
// ================================================================
// 如果当前位置已有堆栈,先释放旧的
if (m_mainThreadStackCycleArray[m_tailPoint] != NULL) {
free(m_mainThreadStackCycleArray[m_tailPoint]);
}
// 保存新堆栈
m_mainThreadStackCycleArray[m_tailPoint] = stackArray;
m_mainThreadStackCount[m_tailPoint] = stackCount;
// ================================================================
// 2. 统计栈顶地址连续重复次数(核心!)
// ================================================================
// 计算上一个位置的索引
uint64_t lastTailPoint = (m_tailPoint + m_cycleArrayCount - 1) % m_cycleArrayCount;
// 获取上一个堆栈的栈顶地址
uintptr_t lastTopStack = 0;
if (m_mainThreadStackCycleArray[lastTailPoint] != NULL) {
lastTopStack = m_mainThreadStackCycleArray[lastTailPoint][0];
}
// 获取当前堆栈的栈顶地址
uintptr_t currentTopStackAddr = stackArray[0];
// 比较栈顶地址
if (lastTopStack == currentTopStackAddr) {
// 栈顶地址相同,累加重复次数
size_t lastRepeatCount = m_topStackAddressRepeatArray[lastTailPoint];
m_topStackAddressRepeatArray[m_tailPoint] = lastRepeatCount + 1;
} else {
// 栈顶地址不同,重置重复次数
m_topStackAddressRepeatArray[m_tailPoint] = 0;
}
// ================================================================
// 3. 移动尾指针
// ================================================================
m_tailPoint = (m_tailPoint + 1) % m_cycleArrayCount;
pthread_mutex_unlock(&m_threadLock);
}
栈顶地址重复次数统计示例
假设连续采集到以下堆栈(简化为栈顶地址):
时间: T0 T50 T100 T150 T200 T250 T300 T350 T400
索引: 0 1 2 3 4 5 6 7 8
堆栈: S0 S1 S2 S3 S4 S5 S6 S7 S8
栈顶: A B C C C C C D D
m_topStackAddressRepeatArray 的值:
[0, 0, 0, 1, 2, 3, 4, 0, 1]
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
A B C C重 C重 C重 C重 D D重
首次 首次 首次 复2 复3 复4 复5 首次 复2
分析:
- S6(索引6)的栈顶地址 C 连续重复了 4 次(从 S2 到 S6)
- 说明主线程在函数 C 上停留了 5 × 50ms = 250ms
- S6 就是最有可能导致卡顿的堆栈(Point Stack)
什么是 Point Stack?
Point Stack(关键堆栈) 是指在一个检查周期内,最有可能导致卡顿的主线程堆栈。
一、核心数据结构
1. 循环数组配置
| 变量名 | 类型 | 说明 |
|---|---|---|
m_cycleArrayCount | int | 循环数组大小(例如:20) |
m_tailPoint | uint64_t | 循环数组尾指针,指向下一个写入位置 |
pthread_mutex_t | m_threadLock | 线程锁,保护循环数组的并发访问 |
循环数组原理:
数组大小 = 检查周期 / 堆栈间隔
例如:1000ms / 50ms = 20
索引: 0 1 2 3 4 ... 19
┌────┬────┬────┬────┬────┬ ─ ─ ┬────┐
堆栈: │ S0 │ S1 │ S2 │ │ │ │ │
└────┴────┴────┴─▲──┴────┴ ─ ─ ┴────┘
│
m_tailPoint
当数组满时,从头开始覆盖(FIFO)
2. 堆栈存储数组(二维数组)
| 变量名 | 类型 | 维度 | 说明 |
|---|---|---|---|
m_mainThreadStackCycleArray | uintptr_t ** | [cycleArrayCount][stackDepth] | 堆栈地址二维数组 |
m_mainThreadStackCount | size_t * | [cycleArrayCount] | 每个堆栈的深度数组 |
数据结构示意:
m_mainThreadStackCycleArray:
[0] → [0x1000, 0x2000, 0x3000, ...] // 第0个堆栈,深度=3
[1] → [0x1000, 0x2000, 0x3000, ...] // 第1个堆栈,深度=3
[2] → [0x1000, 0x2000, 0x4000, ...] // 第2个堆栈,深度=3
...
[19] → NULL // 尚未写入
m_mainThreadStackCount:
[0] = 3 // 第0个堆栈深度
[1] = 3 // 第1个堆栈深度
[2] = 3 // 第2个堆栈深度
...
[19] = 0 // 尚未写入
3. 栈顶重复次数数组
| 变量名 | 类型 | 说明 |
|---|---|---|
m_topStackAddressRepeatArray | size_t * | 每个堆栈的栈顶地址连续重复次数 |
用途: 找出 Point Stack(栈顶重复次数最多的堆栈)
数据示例:
假设连续采集的堆栈栈顶地址:
索引: 0 1 2 3 4
栈顶: A A A B B
m_topStackAddressRepeatArray:
[0] [1] [2] [3] [4]
0 1 2 0 1
解释:
- 索引0: 第一次出现A,重复0次
- 索引1: 第二次出现A(与前一个相同),重复1次
- 索引2: 第三次出现A(与前一个相同),重复2次
- 索引3: 出现B(改变了),重复0次
- 索引4: 第二次出现B(与前一个相同),重复1次
结果:索引2的重复次数最多(2次),所以索引2是Point Stack
4. Point Stack地址重复次数数组
| 变量名 | 类型 | 说明 |
|---|---|---|
m_mainThreadStackRepeatCountArray | int * | Point Stack中每个地址的总重复次数(动态分配) |
用途: 统计 Point Stack 中每个地址在所有堆栈中的总出现次数,识别热点函数
数据示例:
假设有4个堆栈,Point Stack是索引2:
Stack 0: Stack 1: Stack 2(Point): Stack 3:
0x1000 0x1000 0x1000 0x1000
0x2000 0x2000 0x2000 0x2000
0x3000 0x3000 0x3000 0x4000
0x4000 0x5000 0x6000
Point Stack (索引2) 的地址:
[0] = 0x1000
[1] = 0x2000
[2] = 0x3000
统计结果 m_mainThreadStackRepeatCountArray:
[0] = 4 // 0x1000 在4个堆栈中都出现
[1] = 4 // 0x2000 在4个堆栈中都出现
[2] = 3 // 0x3000 在3个堆栈中出现
符号化后:
[0] main (4次) ← 所有堆栈都有,基础函数
[1] viewDidLoad (4次) ← 所有堆栈都有,入口函数
[2] heavyWork (3次) ← 75%的时间在这里,瓶颈!⚠️
算法流程
总体流程图
开始
↓
1. 查找最大重复次数
↓
2. 按时间顺序找出第一个等于最大值的堆栈索引
↓
3. 复制 Point Stack
↓
4. 计算 Point Stack 中每个地址的总重复次数
↓
5. 创建 KSStackCursor 并返回
↓
结束
步骤 1:查找最大重复次数
目的: 找出 m_topStackAddressRepeatArray 中的最大值。
size_t maxValue = 0;
BOOL trueStack = NO;
// 第一次遍历:只找最大值(不记录索引)
for (int i = 0; i < m_cycleArrayCount; i++) {
size_t currentValue = m_topStackAddressRepeatArray[i];
if (currentValue >= maxValue) {
maxValue = currentValue;
trueStack = YES;
}
}
if (!trueStack) {
return NULL; // 没有有效堆栈
}
步骤 2:找出 Point Stack 的索引
目的: 按时间顺序(从新到旧)找第一个重复次数等于 maxValue 的堆栈。
size_t currentIndex = (m_tailPoint + m_cycleArrayCount - 1) % m_cycleArrayCount;
// 第二次遍历:按时间顺序(从新到旧)
for (int i = 0; i < m_cycleArrayCount; i++) {
// 计算真实索引
int trueIndex = (m_tailPoint + m_cycleArrayCount - i - 1) % m_cycleArrayCount;
// 找到第一个等于最大值的
if (m_topStackAddressRepeatArray[trueIndex] == maxValue) {
currentIndex = trueIndex;
break; // 找到最新的,立即停止
}
}
索引计算公式:
trueIndex = (m_tailPoint + m_cycleArrayCount - i - 1) % m_cycleArrayCount
参数说明:
- m_tailPoint: 下一个要写入的位置
- i: 遍历变量(0 = 最新,1 = 次新,...)
- m_cycleArrayCount: 数组大小(如20)
例子:
假设 m_tailPoint = 1, m_cycleArrayCount = 5
i=0: trueIndex = (1+5-0-1) % 5 = 0 → 最新堆栈
i=1: trueIndex = (1+5-1-1) % 5 = 4 → 次新堆栈
i=2: trueIndex = (1+5-2-1) % 5 = 3 → 第三新堆栈
i=3: trueIndex = (1+5-3-1) % 5 = 2 → 第四新堆栈
i=4: trueIndex = (1+5-4-1) % 5 = 1 → 最旧堆栈(空)
步骤 3:复制 Point Stack
size_t stackCount = m_mainThreadStackCount[currentIndex];
size_t pointThreadSize = sizeof(uintptr_t) * stackCount;
uintptr_t *pointThreadStack = (uintptr_t *)malloc(pointThreadSize);
// 复制堆栈地址
for (size_t idx = 0; idx < stackCount; idx++) {
pointThreadStack[idx] = m_mainThreadStackCycleArray[currentIndex][idx];
}
步骤 4:计算地址总重复次数
三层循环统计:
// 分配重复次数数组
m_mainThreadStackRepeatCountArray = (int *)malloc(stackCount * sizeof(int));
memset(m_mainThreadStackRepeatCountArray, 0, stackCount * sizeof(int));
// 外层循环:遍历 Point Stack 的每个地址
for (size_t i = 0; i < stackCount; i++) {
uintptr_t targetAddress = m_mainThreadStackCycleArray[currentIndex][i];
// 中层循环:遍历循环数组中的每个堆栈
for (int innerIndex = 0; innerIndex < m_cycleArrayCount; innerIndex++) {
size_t innerStackCount = m_mainThreadStackCount[innerIndex];
// 内层循环:遍历当前堆栈的每个地址
for (size_t idx = 0; idx < innerStackCount; idx++) {
// 比较是否匹配
if (targetAddress == m_mainThreadStackCycleArray[innerIndex][idx]) {
m_mainThreadStackRepeatCountArray[i] += 1;
}
}
}
}
算法分析:
- 时间复杂度:O(n × m × k)
-
- n = Point Stack 深度(通常 < 100)
- m = 循环数组大小(通常 20)
- k = 平均堆栈深度(通常 < 50)
- 实际数据量很小,性能可接受
步骤 5:创建 KSStackCursor
KSStackCursor *pointCursor = (KSStackCursor *)malloc(sizeof(KSStackCursor));
kssc_initWithBacktrace(pointCursor, pointThreadStack, (int)stackCount, 0);
return pointCursor;
作用: 将原始堆栈数组包装成 KSCrash 能使用的标准格式。
至于堆栈的获取,可以参考我的另一篇文章ARM64 调用栈回溯原理