logo
积分中心
ArkRuntime Freeze问题的多维度排查与分析路径
头像 鸿蒙小助手 2026-10-21 14:53:28    发布
21 浏览 4 点赞 3 收藏

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

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:13 秒到 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 才发现看日志找频繁调用的接口并优化
案例五内存泄漏导致频繁 GCtrace 显示主线程一直在触发 GC排查 JS/Native 内存泄漏
案例七高优先级任务过多freeze 文件任务队列里 VIP+Immediate+High 总数 > 400减少高优先级任务数量

      通用规律:只要"栈顶在方舟运行时"且确认不是死锁,就沿着"主线程在忙什么"这条线去找——循环、正则、序列化、GC、任务队列拥挤,总有一个是元凶。


九、一句话总结

      freeze 栈顶在方舟运行时,去掉 jsruntime 栈帧后看 3S 和 6S 的应用代码行:相同 = 主线程被这段代码卡住(不是死锁)。找到那段代码,加超时保护 / 优化算法 / 挪到 TaskPool。

      判别三条线索:① 栈顶在方舟运行时;② 3S 与 6S 指向同一行应用代码;③ 栈中无持锁/等待帧。三条齐备 = 主线程繁忙型冻屏,沿"主线程在忙什么"定位即可。

----------------------------------------------------------------------------------------------------------

🔗 官网开发者学堂视频:https://developer.huawei.com/consumer/cn/training/result?type2List=201783644516849879&orderBy=1&courseType=5?ha_source=zxqy-IT&ha_sourceId=89000468

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

【扫码加入 HarmonyOS DFX 技术交流群】

©本站发布的所有内容,包括但不限于文字、图片、音频、视频、图表、标志、标识、广告、商标、商号、域名、软件、程序等,除特别标明外,均来源于网络或用户投稿,版权归原作者或原出处所有。我们致力于保护原作者版权,若涉及版权问题,请及时联系我们进行处理。
分类
HarmonyOS
地址:北京市朝阳区北三环东路三元桥曙光西里甲1号第三置业A座1508室 电话:13391790444或(010)62178877
版权所有:电脑商情信息服务集团 北京赢邦策略咨询有限责任公司
声明:本媒体部分图片、文章来源于网络,版权归原作者所有,我司致力于保护作者版权,如有侵权,请与我司联系删除

京ICP备:2022009079号-2

京公网安备:11010502051901号

ICP证:京B2-20230255