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

https://developer.huawei.com/consumer/cn/forum/topic/0208221853909201267?ha_source=csdn&ha_sourceId=70000010

上一篇讲了用AppFreeze 定位“锁竞争”引发的冻屏问题,这一篇将分析“UI 渲染过载”引发的冻屏问题。在 ArkUI 开发中,频繁的状态更新(State Update)和列表刷新(ForEach/LazyForEach)会导致主线程陷入密集的 Diff 计算和渲染任务,从而引发卡顿。本文将介绍如何利用采样栈识别 UI 渲染热点。


1. 问题现象

       用户在触发某项业务操作后,应用界面出现明显卡死,随后进入无响应状态,最终被系统强制退出。开发者需要从系统生成的 AppFreeze 日志中,找出导致主线程卡死的根因。

2. 问题分析:从快照到采样

第一步:常规快照排查(初步诊断)

       查看 AppFreeze 日志中的 THREAD_BLOCK_3S 和 THREAD_BLOCK_6S 主线程堆栈快照。

     • THREAD_BLOCK_3S 部分

Tid:62838, Name:pfreezeanalysis
    state=R, utime=802, stime=42, priority=-54, nice=-10, clk=100
    #00 pc 00000000000df550 /system/lib/ld-musl-aarch64.so.1(arena_slab_reg_alloc_batch+316)(58b5bc0b32f5d8462c0e616fbc5ee53a)
    #01 pc 00000000000df018 /system/lib/ld-musl-aarch64.so.1(arena_cache_bin_fill_small+504)(58b5bc0b32f5d8462c0e616fbc5ee53a)
    #02 pc 00000000001042d4 /system/lib/ld-musl-aarch64.so.1(je_tcache_alloc_small_hard+268)(58b5bc0b32f5d8462c0e616fbc5ee53a)
    #03 pc 00000000000bab40 /system/lib/ld-musl-aarch64.so.1(malloc_default+4644)(58b5bc0b32f5d8462c0e616fbc5ee53a)
    #04 pc 00000000000bbf98 /system/lib/ld-musl-aarch64.so.1(je_malloc+676)(58b5bc0b32f5d8462c0e616fbc5ee53a)
    #05 pc 00000000000b0050 /system/lib64/chipset-sdk-sp/libc++.so(operator new(unsigned long)+24)(df2393506d17d97a741820dba41a227fe61b1f00)
    #06 pc 0000000001340f08 /system/lib64/platformsdk/libace_compatible.z.so(OHOS::Ace::RefPtr<OHOS::Ace::NG::TextEventHub> OHOS::Ace::Referenced::MakeRefPtr<OHOS::Ace::NG::TextEventHub>()+32)(37cb86795b559fcddf104355c179fdbc)
    #07 pc 00000000009d40a8 /system/lib64/platformsdk/libace_compatible.z.so(OHOS::Ace::NG::FrameNode::CreateEventHubInner()+740)(37cb86795b559fcddf104355c179fdbc)
    #08 pc 0000000000d86e48 /system/lib64/platformsdk/libace_compatible.z.so(OHOS::Ace::NG::TextModelNG::Create(std::__h::basic_string<char16_t, std::__h::char_traits<char16_t>, std::__h::allocator<char16_t>> const&)+928)(37cb86795b559fcddf104355c179fdbc)
    #09 pc 0000000000d8565c /system/lib64/platformsdk/libace_compatible.z.so(OHOS::Ace::Framework::JSText::Create(OHOS::Ace::Framework::JsiCallbackInfo const&)+564)(37cb86795b559fcddf104355c179fdbc)
    #10 pc 0000000003859888 /system/lib64/platformsdk/libace_compatible.z.so(OHOS::Ace::Framework::JsiClass<OHOS::Ace::Framework::JSText>::JSStaticMethodCallback(panda::JsiRuntimeCallInfo*)+216)(37cb86795b559fcddf104355c179fdbc)
    #11 pc 00000000007a0234 /system/lib64/platformsdk/libark_jsruntime.so(panda::Callback::RegisterCallback(panda::ecmascript::EcmaRuntimeCallInfo*)+336)(528445894c7c84ccb26a5e5f1d8f925b)
    #12 pc 0000000000e949e8 /system/lib64/module/arkcompiler/stub.an(RTStub_PushCallArgsAndDispatchNative+40)
    #13 pc 00000000005a4a2c /system/lib64/module/arkcompiler/stub.an(BCStub_HandleCallthis1withnameImm8Id16V8V8StwCopy+424)
    #14 at anonymous (entry|entry|1.0.0|src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ts:272:26)

• THREAD_BLOCK_6S 部分

Tid:62838, Name:pfreezeanalysis
    state=R, utime=1024, stime=102, priority=-54, nice=-10, clk=100
    #00 pc 0000000000adee1c /system/lib64/platformsdk/libace_compatible.z.so(OHOS::Ace::NG::TextPattern::RecoverCopyOption()+212)(37cb86795b559fcddf104355c179fdbc)
    #01 pc 000000000285acd4 /system/lib64/platformsdk/libace_compatible.z.so(OHOS::Ace::NG::TextPattern::OnModifyDone()+1012)(37cb86795b559fcddf104355c179fdbc)
    #02 pc 0000000000aaafd4 /system/lib64/platformsdk/libace_compatible.z.so(OHOS::Ace::NG::FrameNode::MarkModifyDone()+224)(37cb86795b559fcddf104355c179fdbc)
    #03 pc 0000000001346ad8 /system/lib64/platformsdk/libace_compatible.z.so(OHOS::Ace::NG::IfElseModelNG::Pop()+1360)(37cb86795b559fcddf104355c179fdbc)
    #04 pc 000000000368d17c /system/lib64/platformsdk/libace_compatible.z.so(panda::Local<panda::JSValueRef> OHOS::Ace::Framework::JsiClass<OHOS::Ace::Framework::JSContainerBase>::StaticMethodCallback<void>(panda::JsiRuntimeCallInfo*)+1048)(37cb86795b559fcddf104355c179fdbc)
    #05 pc 00000000007a0234 /system/lib64/platformsdk/libark_jsruntime.so(panda::Callback::RegisterCallback(panda::ecmascript::EcmaRuntimeCallInfo*)+336)(528445894c7c84ccb26a5e5f1d8f925b)
    #06 pc 0000000000e949e8 /system/lib64/module/arkcompiler/stub.an(RTStub_PushCallArgsAndDispatchNative+40)
    #07 pc 00000000005a4658 /system/lib64/module/arkcompiler/stub.an(BCStub_HandleCallthis0withnameImm8Id16V8StwCopy+400)
    #08 at l199 (entry|entry|1.0.0|src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ts:274:22)
    #09 at anonymous (/usr1/hmos_for_system/src/increment/sourcecode/out/generic_generic_arm_64only/general_all_phone_standard_2d/obj/foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/stateMgmt.js:6203:1)
    #10 pc 0000000000b726f4 /system/lib64/module/arkcompiler/stub.an(BuiltinStub_ArrayForEachStwCopy+788)
    #11 pc 0000000000486190 /system/lib64/module/arkcompiler/stub.an(BCStub_HandleCallthis1Imm8V8V8StwCopy+532)
    #12 at forEachUpdateFunction (/usr1/hmos_for_system/src/increment/sourcecode/out/generic_generic_arm_64only/general_all_phone_standard_2d/obj/foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/stateMgmt.js:6200:1)
    #13 at anonymous (entry|entry|1.0.0|src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ts:276:18)

证据 1:两次堆栈不一致

时间点

线程状态

关键调用栈 (Top Frames)

行为分析

THREAD_BLOCK_3S

R (Running)

je_malloc → operator new → TextModelNG::Create → JSText::Create

主线程正在创建新的 UI 组件实例(内存分配与对象构建)。

THREAD_BLOCK_6S

R (Running)

TextPattern::OnModifyDone → FrameNode::MarkModifyDone → ForEach 相关回调

主线程正在渲染/更新已创建的 UI 组件(布局计算与绘制标记)。

       深度解析

 • 状态均为 R (Running):说明主线程并没有卡在某个锁(Lock)或阻塞调用上,而是一直在“干活”。

 • 堆栈不一致:3秒时在做“创建”,6秒时在做“更新”。这表明主线程在极短的时间内,反复交替执行组件创建和渲染更新的操作。

 • 结论:常规快照只能看到“点”,无法看到“面”。仅凭快照无法判断具体是哪个环节耗时过长,必须引入增强日志中的多帧采样统计来寻找“热点”。

第二步:深入挖掘(增强日志采样分析)

       开启增强日志后,查看采样频率最高的关键栈帧序列(Top Hotspot)。

       证据 2:增强日志中的“热点”栈帧

#00 OHOS::Ace::NG::TextPattern::RecoverCopyOption()
#01 OHOS::Ace::NG::TextPattern::OnModifyDone()      <-- UI渲染核心
#02 OHOS::Ace::NG::FrameNode::MarkModifyDone()
#03 OHOS::Ace::NG::IfElseModelNG::Pop()
#04 OHOS::Ace::Framework::JsiClass::StaticMethodCallback()
#05 panda::Callback::RegisterCallback()
#06 RTStub_PushCallArgsAndDispatchNative()
#07 BCStub_HandleCallthis0withnameImm8Id16V8StwCopy()
#08 at l199 (appfreeze_threadblock.ts:274:22)       <-- 业务代码入口
#09 at anonymous (.../stateMgmt.js:6203:1)
#10 BuiltinStub_ArrayForEachStwCopy()               <-- 关键线索:ForEach
#11 BCStub_HandleCallthis1Imm8V8V8StwCopy()
#12 at forEachUpdateFunction (.../stateMgmt.js:6200:1)
#13 at anonymous (appfreeze_threadblock.ts:276:18)

       关键线索提取:

   • UI 层过载:栈顶频繁出现 TextPattern::OnModifyDone 和 FrameNode::MarkModifyDone,这是 ArkUI 渲染管线中处理文本模式变更和节点标记的核心函数。

   • 业务层定位:明确指向 appfreeze_threadblock.ts 第 274-276 行。

   • 循环特征:出现 BuiltinStub_ArrayForEachStwCopy 和 forEachUpdateFunction,强烈暗示代码中存在列表遍历或状态更新循环

   • 推测:业务代码可能在循环中频繁修改状态,导致 ForEach 不断触发 UI 重建,进而引发大量的 Text 组件渲染。

第三步:锁定根因(代码审查)

       根据日志定位到具体文件 appfreeze_threadblock.ts,查看源码:

       证据 3:问题代码片段

@State taskList: string[] = [];

// 问题点:循环中连续修改 @State 数组
public generateTaskList() {
    for (let taskIndex = 0; taskIndex < TASK_COUNT; taskIndex++) {
        this.taskList.push(taskIndex + ''); // 每次 push 都触发一次 UI 刷新
    }
}

@Builder
renderTaskList() {
    // 问题点:基础 ForEach,非虚拟化,全量渲染
    ForEach(this.taskList, (item: string, index) => {
        Text(item); 
    })
}

       根因总结:

   • 频繁状态更新:generateTaskList 在循环中执行 push,每执行一次 @State 变更通知,都会触发一次 ForEach 的 diff 计算和 UI 更新。如果有 1000 个元素,主线程就要处理 1000 次微小的 UI 更新请求。

   • 过度绘制与实例化:ForEach 会为每个元素创建独立的 Text 组件实例。在数据量大时,这造成了巨大的内存分配压力(对应快照中的 malloc/new)和渲染负载。

   • 缺乏虚拟化:使用了基础的 ForEach 而非 LazyForEach,即使屏幕外不可见的元素也被创建和维护,严重拖慢主线程。

3. 解决方案与实践案例

优化策略

• 使用虚拟化列表:使用 List + LazyForEach 替代 ForEach,实现按需加载,仅渲染可视区域内的组件。

• 批量状态更新:避免在循环中修改 @State。应先构建好完整的数据数组,再一次性赋值,减少 UI 刷新次数。

• 简化组件结构:确保 ListItem 内部结构扁平化,减少布局计算复杂度。

优化后代码示例

import { LazyForEach } from '@kit.ArkUI';

// 1. 实现数据源类,满足 IObjectHandler 接口
class MyDataSource implements IObjectHandler<number> {
  private dataArray: number[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): number {
    if (index < 0 || index >= this.dataArray.length) {
      return 0;
    }
    return this.dataArray[index];
  }

  // 更新数据并通知视图
  appendData(newData: number[]): void {
    this.dataArray.push(...newData);
    this.onDataChanged(); // 触发 LazyForEach 重新拉取数据
  }
  
  // 注意:实际项目中需根据版本确认 onDataChanged 的具体实现方式
  private onDataChanged(): void {} 
}

@Entry
@Component
struct PageSecond {
  // 2. 使用 LazyForEach 数据源
  lazyDataSource: MyDataSource = new MyDataSource();

  aboutToAppear() {
    // 3. 批量生成数据,一次性更新
    const newData = [];
    for (let i = 0; i < 1000; i++) {
      newData.push(i);
    }
    this.lazyDataSource.appendData(newData);
  }

  build() {
    List({ space: 5 }) {
      // 4. 使用 LazyForEach 进行虚拟化渲染
      LazyForEach(this.lazyDataSource, 
        // 渲染函数:仅当列表项进入可视区域时调用
        (item: number) => {
          ListItem() {
            Text(`Task ${item}`)
              .fontSize(16)
              .margin({ top: 5, bottom: 5 })
          }
        },
        // 唯一标识函数:用于优化 diff 算法
        (item: number) => item.toString()
      )
    }
    .width('100%')
    .height('100%')
    .edgeEffect(EdgeEffect.Spring)
  }
}

4. 总结与启示

       通过这个案例,开发者展示了如何利用 AppFreeze 增强日志解决复杂的性能问题:

   • 快照看状态:通过对比不同时间点的快照,判断线程是“阻塞”还是“繁忙”。

   • 采样看热点:通过增强日志的统计功能,快速定位高频执行的函数栈(Hotspot)。

   • 代码看逻辑:结合业务代码,发现“循环修改状态”+“非虚拟化列表”的性能陷阱。

   • 架构做优化:引入虚拟化列表(LazyForEach)和批量更新机制,从根本上解决主线程负载过高的问题。

       开发者贴士:在开发长列表或动态内容时,请始终优先考虑 LazyForEach,并避免在循环中直接操作 @State 变量。

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

      🔗 官网开发者学堂视频:华为开发者学堂

     🔗 社区DFX专题文章:  华为开发者问答 | 华为开发者联盟

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

Logo

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

更多推荐