鸿蒙小助手 2026-10-21 14:53:28 发布本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:
ArkRuntime Freeze问题的多维度排查与分析路径-华为开发者话题 | 华为开发者联盟
一、案例背景
某应用使用过程中偶发 UI 卡死,WatchDog 上报 THREAD_BLOCK_6S 应用冻屏(AppFreeze)事件。
开发者拿到 freeze 堆栈,发现它同时包含 3S 栈(warning 栈)和 6S 栈(error 栈),两份栈的栈顶都是同一个函数:
#00 ... libark_jsruntime.so(Builtins::JSStableArray::IndexOfBigInt+...)
#01 ... libark_jsruntime.so(Interpreter::Execute+...)
#02 ... GeneratorReEnterInterpreter
#03 at findXXXList (entry|feat_xxx|1.0.0|src/main/ets/model/LeakXXXModel.ts:440:1)
...
#20 ... libappkit_native.z.so(MainThread::Start)第一反应:栈顶是 JSStableArray::IndexOfBigInt,看起来是 ArkRuntime(方舟运行时)的问题,是不是虚拟机有 bug?
真相:方舟运行时本身没有 bug。根因是开发者在主线程里写了一个非常大的 for/while 循环,循环里调用了 IndexOfBigInt 这类 Builtins 接口,单次循环不慢,但循环次数巨大,把主线程占满了 6 秒以上,WatchDog 的判活任务迟迟排不进主线程队列,于是上报冻屏。
这个案例讲的就是:如何通过"3S 栈和 6S 栈指向同一行应用代码"这一特征,快速识别出"主线程被大循环卡住",并给出修复方案。
二、关键认知:栈顶在方舟运行时 ≠ 方舟运行时的 bug
这是理解整个案例(乃至整篇"栈顶在方舟运行时的应用冻屏"文档)的前提。
方舟运行时(libark_jsruntime.so、libace_napi.z.so)是 ArkTS/JS 代码的执行引擎。如果 freeze 栈顶在方舟运行时,绝大多数情况下不是引擎的 bug,而是开发者写的 ArkTS/JS 代码让主线程繁忙。引擎只是忠实地在执行你的代码。
| 错误直觉 | 实际行为 |
| "栈顶在 libark_jsruntime.so 是虚拟机卡死了" | 虚拟机在执行你的 JS 代码,是你的代码让它忙不过来 |
| "IndexOfBigInt 这种 Builtins 接口应该很快" | 单次确实快,但放在一个几十万次的循环里就慢了 |
| "3S 和 6S 栈相同就是死锁" | 不一定。没有持锁/等待帧时,更可能是同一行代码还没执行完 |
一句话记忆:freeze 栈顶在方舟运行时 → 去掉 jsruntime 栈帧后,看 3S 和 6S 指向的应用代码行是否相同 → 相同 = 主线程被这段代码卡住(大概率是循环/耗时调用),而不是死锁。
三、问题代码(案发现场)
开发者在主线程实现了一个 findXXXList 接口,功能是返回 [start, end] 区间中"缺失消息"组成的列表。为了逻辑直观,直接用了一个 while 循环遍历查找:
// ❌ 问题代码(LeakXXXModel.ts:440)
public async findXXXList(indexList, start, end) {
while (start <= end) {
// 在 indexList 里查找当前下标是否缺失,内部调用 Array.indexOf
// (底层对应 Builtins::JSStableArray::IndexOfBigInt)
if (indexList.indexOf(start) === -1) {
this.missingList.push(start);
}
start++;
}
return this.missingList;
}为什么卡 6 秒:当 indexList 很大、[start, end] 区间也很宽时,while 循环要跑几十万次,每次循环里又对一个大数组做 indexOf(时间复杂度 O(n)),整体复杂度接近 O(n²)。主线程一直在这一个函数里出不来,WatchDog 的判活任务(高优先级 Timer)排在主线程任务队列里得不到执行。
堆栈对应关系:
#03 at findXXXList (LeakXXXModel.ts:440) ← 开发者代码:大循环 + indexOf
#02 GeneratorReEnterInterpreter ← ArkTS 异步函数回到解释器
#01 Interpreter::Execute ← 方舟解释器在执行 JS 字节码
#00 JSStableArray::IndexOfBigInt ← indexOf 的 Builtins 实现(循环里被反复调用)四、定位手段
4.1 从 3S/6S 栈识别"主线程被一段代码卡住"模式
freeze 堆栈有三条判别线索:
| 线索 | 本案例特征 | 含义 |
| 栈顶在方舟运行时 | JSStableArray::IndexOfBigInt / Interpreter::Execute | 阻塞点在执行 ArkTS/JS 代码 |
| 3S 栈与 6S 栈的应用代码行相同 | 两份栈都指向 LeakXXXModel.ts:440:1 | 3 秒到 6 秒之间,主线程始终没离开这一行 |
| 栈中没有持锁/等待帧 | 没有 pthread_mutex_lock / futex_wait | 排除死锁,指向"忙等"而非"锁等" |
三条线索齐备 → 直接判定:主线程被 LeakXXXModel.ts:440 这段代码卡住,且不是死锁,而是执行耗时过长。
4.2 为什么"3S 和 6S 指向同一行"是关键信号
WatchDog 的检测机制:每隔 3 秒向主线程队列抛一个判活任务。3 秒没执行 → 上报 THREAD_BLOCK_3S;6 秒还没执行 → 上报 THREAD_BLOCK_6S。两份栈是两个时刻的"快照"。
• 两份栈的应用代码行相同 → 这 3 秒内主线程一直卡在同一行,说明这行代码本身耗时极长(循环、正则、序列化等)。
• 两份栈的应用代码行不同 → 主线程还在推进业务,只是任务太重,需要结合 trace 看是不是频繁调用导致的繁忙。
本案例属于前者,所以只用"一板斧:看 freeze 栈"就能定位到具体代码行。
4.3 反向定位开发者代码
从堆栈里找带 .ts 的行:
#03 at findXXXList (entry|feat_xxx|1.0.0|src/main/ets/model/LeakXXXModel.ts:440:1)直接定位到 LeakXXXModel.ts:440 的 findXXXList 函数,看它在做什么——发现一个 while 循环里嵌套 indexOf,数据量大时复杂度爆炸。
4.4 用 trace 交叉验证(二板斧)
如果只看栈还不够确定,可以抓 freeze 前的 trace 交叉验证。本案例的 trace 会显示:主线程在这 6 秒里没有大段空白(不是在持锁等待),也没有频繁 GC,而是一直在 running、连续执行同一个函数。这和"大循环"的特征完全吻合,可以进一步坐实根因。
五、修复方案
5.1 核心原则
不要让主线程长时间停留在一个同步耗时函数里。 有三种手段,按推荐顺序:
1. 加超时保护:给耗时循环加一个定时器,超时即跳出(改动最小,适合短期内无法重构逻辑的场景)。
2. 优化算法:降低循环复杂度(从 O(n²) 降到 O(n)),减少单次执行耗时。
3. 挪到子线程:把整个耗时函数放到 TaskPool/Worker 执行,不阻塞主线程。
5.2 修复后代码(方案一:加超时保护)
这是来源文档给出的规避方案。设置一个定时器做超时检测,当循环代码执行超时后直接跳出,避免冻屏:
// ✅ 修复后(方案一):加超时定时器,超时即跳出
public async findXXXList(indexList, start, end) {
let needBreak = false;
setTimeout(() => {
needBreak = true;
}, 100); // 定时器:100ms 超时
while (start <= end) {
// do some thing
start++;
if (needBreak) {
break; // 超时退出,避免阻塞主线程
}
}
}while 还是那个 while,但有了 100ms 的"逃生出口",主线程不会再被卡 6 秒,WatchDog 的判活任务能及时得到执行,不再上报冻屏。
5.3 修复后代码(方案二:优化算法,推荐)
超时保护只是规避,根本问题是算法复杂度。把"在大数组里反复 indexOf"换成"用 Set 做一次 O(1) 查找",整体复杂度从 O(n²) 降到 O(n):
// ✅ 修复后(方案二):用 Set 替代 indexOf,复杂度 O(n²) → O(n)
public async findXXXList(indexList, start, end) {
const indexSet = new Set(indexList); // 一次建索引,O(n)
for (let i = start; i <= end; i++) {
if (!indexSet.has(i)) { // O(1) 查找
this.missingList.push(i);
}
}
return this.missingList;
}
数据量大时,执行时间从秒级降到毫秒级,从根上消除冻屏。
5.4 修复后代码(方案三:挪到 TaskPool)
如果逻辑本身确实重、无法优化到毫秒级,就把它整体挪到子线程:
// ✅ 修复后(方案三):整个函数挪到 TaskPool
import taskpool from '@ohos.taskpool';
@Concurrent
function findXXXListOnWorker(indexList, start, end): number[] {
const indexSet = new Set(indexList);
const missing: number[] = [];
for (let i = start; i <= end; i++) {
if (!indexSet.has(i)) missing.push(i);
}
return missing;
}
public async findXXXList(indexList, start, end) {
const task = new taskpool.Task(findXXXListOnWorker, indexList, start, end);
this.missingList = await taskpool.execute(task) as number[];
return this.missingList;
}阻塞的是 worker 线程不是 UI 线程,主线程继续响应输入。
优先级:能优化算法就优化算法(根治);短期不能重构就加超时保护(规避);逻辑本身重就挪到 TaskPool/Worker(隔离)。
六、修复后验证
6.1 现象验证
• 触发原来卡顿的场景,UI 不再卡死,输入框、滚动列表照常响应
• WatchDog 不再上报 THREAD_BLOCK_6S / THREAD_BLOCK_3S 事件
• hilog | grep -i "FREEZE" 无新上报
6.2 堆栈验证
如果再次抓 freeze 堆栈(理论上不应该有),应该看到:
• 3S 栈和 6S 栈不再指向同一行应用代码(加超时保护后,循环会主动让出)
• 栈顶不再长时间停在 JSStableArray::IndexOfBigInt(优化算法后不再反复 indexOf)
• 若用 TaskPool 方案,主线程栈上根本不会出现 findXXXList,它只出现在 worker 线程栈上
七、一张图记住这个案例
┌─────────────────────────────────────────────────────────┐
│ 问题现象: 偶发 UI 卡死,WatchDog 上报 THREAD_BLOCK_6S │
└──────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ freeze 堆栈: 栈顶 JSStableArray::IndexOfBigInt │
│ (看似方舟运行时问题,实际是主线程被大循环卡住) │
└──────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 三条线索: │
│ 1. 栈顶在方舟运行时 (IndexOfBigInt) │
│ 2. 3S 栈与 6S 栈指向同一行 LeakXXXModel.ts:440 │
│ 3. 栈中无持锁/等待帧 → 排除死锁 │
└──────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 根因: findXXXList 主线程 while + indexOf,O(n²) 耗时 │
│ → 主线程 6 秒没出来 → WatchDog 上报 FREEZE │
└──────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 修复: 加超时保护 / 用 Set 优化算法 / 挪到 TaskPool │
│ 验证: UI 不再卡顿,FREEZE 不再上报 │
└─────────────────────────────────────────────────────────┘
八、延伸:同文档里的其他"主线程繁忙"模式
来源文档里列举了多个同类案例,判别思路一致——栈顶在方舟运行时 + 3S/6S 栈或 trace 暴露主线程繁忙,只是"繁忙的来源"不同:
| 案例 | 繁忙来源 | 典型特征 | 修复方向 |
| 案例二 | 频繁跨线程传输数据 | trace 显示主线程密集调用反序列化接口 | 减少同一时间段频繁跨线程传输 |
| 案例三 | 耗时正则匹配 | 栈顶在正则执行,trace 显示一直跑正则 | 优化正则 / 挪到 TaskPool |
| 案例四 | 循环调用某接口 | freeze 栈和 trace 都定界不了,靠 HiLog 才发现 | 看日志找频繁调用的接口并优化 |
| 案例五 | 内存泄漏导致频繁 GC | trace 显示主线程一直在触发 GC | 排查 JS/Native 内存泄漏 |
| 案例七 | 高优先级任务过多 | freeze 文件任务队列里 VIP+Immediate+High 总数 > 400 | 减少高优先级任务数量 |
通用规律:只要"栈顶在方舟运行时"且确认不是死锁,就沿着"主线程在忙什么"这条线去找——循环、正则、序列化、GC、任务队列拥挤,总有一个是元凶。
九、一句话总结
freeze 栈顶在方舟运行时,去掉 jsruntime 栈帧后看 3S 和 6S 的应用代码行:相同 = 主线程被这段代码卡住(不是死锁)。找到那段代码,加超时保护 / 优化算法 / 挪到 TaskPool。
判别三条线索:① 栈顶在方舟运行时;② 3S 与 6S 指向同一行应用代码;③ 栈中无持锁/等待帧。三条齐备 = 主线程繁忙型冻屏,沿"主线程在忙什么"定位即可。
----------------------------------------------------------------------------------------------------------

🔗 社区DFX专题文章: https://developer.huawei.com/consumer/cn/forum/subject/2101218731402391001?ha_source=zxqy-IT&ha_sourceId=89000468


【扫码加入 HarmonyOS DFX 技术交流群】
相关推荐
鸿蒙小助手
17
0
鸿蒙小助手
22
0
鸿蒙小助手
30
0
鸿蒙小助手
16
0
鸿蒙小助手
25
0
鸿蒙小助手
致力于为鸿蒙开发者谋福利
帖子
提问
粉丝
RN 性能调优之滑动卡顿问题介绍
2026-10-21 14:43:44 发布RN 性能调优之点击响应和完成时延分析介绍
2026-10-21 14:33:16 发布



京公网安备:11010502051901号