logo
积分中心
如何通过日志分析定位onWillChange深拷贝引发的GC卡死
头像 鸿蒙小助手 2026-10-21 16:05:53    发布
27 浏览 4 点赞 4 收藏

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

如何通过日志分析定位onWillChange深拷贝引发的GC卡死-华为开发者话题 | 华为开发者联盟


一、案例背景

      某应用使用 RichEditor 组件的 StyledString 模式,用户在输入框中每打一个字,UI 就会偶发冻结 5 秒以上,WatchDog 上报 FREEZE 事件。

      开发者拿到 freeze 堆栈,长这样:


#00 libace_engine.so!~SpanItem()
#01 libace_engine.so!std::vector<RefPtr<SpanItem>>::clear()
#02 libace_engine.so!~MutableSpanString()
#03 ... (GC finalization 上下文)

      第一反应:堆栈停在 ~SpanItem(),是不是 SpanItem 析构有 bug?

      真相:SpanItem 析构本身没 bug。根因是开发者在 onWillChange 回调里调用了 20 次 getStyledString(),堆积了大量临时副本,GC 回收时析构链重入 JS 引擎死锁。

      这个案例讲的就是:如何从 freeze 现象一路追到这 20 次调用。

二、关键认知:getStyledString 是"拍快照"不是"拿引用"

      这是理解整个案例的前提。


// 开发者直觉上以为:
let s = this.controller.getStyledString();  // 拿到内部对象的引用?
s.setString("hack");                         // 以为能改内部状态?
// 实际行为:
// getStyledString() 每次都"拍一张快照"——深拷贝整个 StyledString
// 返回的是全新副本,改它不影响 RichEditor 内部
设计目标为什么这么设计
状态隔离开发者拿到的副本可以随便改,不会意外破坏 RichEditor 内部状态
数据一致性内部状态只能通过 setStyledString 等显式接口修改
代价每次调用 = 1 个新 MutableSpanString + N 个 SpanItem 深拷贝 + N×K 个 JS 回调句柄副本

      一句话记忆:getStyledString() 不是"获取引用",是"获取快照"。调 N 次 = 拍 N 张快照 = N 份临时副本。

三、问题代码(案发现场)

      开发者在 onWillChange 中为了拿三个不同的信息,调用了三次 getStyledString(),外面又套了一个循环:


// ❌ 问题代码
onWillChange: (value: StyledStringChangeValue) => {
    for (let i = 0; i < 7; i++) {                          // 循环 7 次
        let styled = this.controller.getStyledString();    // 第 1 次深拷贝
        let text = this.controller.getStyledString().getString();   // 第 2 次
        let length = this.controller.getStyledString().length;      // 第 3 次
        this.validate(styled, i);
    }
    return true;
}

      算一笔账:一次文本变化 = 7 次循环 × 3 次调用 = 21 次深拷贝。

      假设 StyledString 里有 10 个 GestureSpan,每个带 onClick 回调:

  • 每次调用产生:10 个 SpanItem + 10 个 CopyableGlobal(JS 句柄)

  • 21 次调用产生:210 个 SpanItem + 210 个 JS 句柄,全部等待 GC 回收

  • GC 回收时,CopyableGlobal::Reset() 要重入 JS 引擎,而 GC 本身已经持有了 JS 引擎锁 → 死锁 → UI Freeze

四、修复方案

4.1 核心原则

      一次文本变化只调用一次 getStyledString(),结果在本次回调内复用。

4.2 修复后代码


// ✅ 修复后
onWillChange: (value: StyledStringChangeValue) => {
    let styled = this.controller.getStyledString();  // 仅 1 次深拷贝
    let text = styled.getString();                   // 复用
    let length = styled.length;                      // 复用
    for (let i = 0; i < 7; i++) {
        this.validate(styled, i);                    // 复用同一份
    }
    return true;
}

      调用次数从 21 降到 1,临时对象从 210 份降到 10 份,GC 压力下降 95%。

4.3 更优方案:用回调参数替代

      其实本案例根本不需要调 getStyledString()——onWillChange 的回调参数 value 已经带了变化信息:


// ✅✅ 更优:完全不用 getStyledString
onWillChange: (value: StyledStringChangeValue) => {
    let range = value.range;                            // 无拷贝
    let newText = value.replacementString.getString();  // 无拷贝
    // 如果业务只关心变化范围和替换内容,这就够了
    return true;
}

      优先级:能用回调参数就别用 getStyledString();必须用就只调一次。

五、一句话总结

      getStyledString() 是拍快照不是拿引用。一次文本变化只拍一次,能拿回调参数就别拍。

      定位手段:hilog -T C0391A | grep -E "BeforeSSChange|GetStyledString|AfterSSChange",看 Before 和 After 之间的调用数,>5 就是高风险。

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

🔗 官网开发者学堂视频: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