本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:

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 指向同一行应用代码;③ 栈中无持锁/等待帧。三条齐备 = 主线程繁忙型冻屏,沿"主线程在忙什么"定位即可。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐