AppFreeze增强采样栈,让你的鸿蒙应用“卡住”无处遁形

0 评论 126 浏览 0 收藏 26 分钟

在利用AppFreeze日志分析冻屏问题时,传统排查手段往往受限于日志信息的匮乏:当接收到THREAD_BLOCK_6S故障时,系统仅能提供两次主线程调用栈快照。一旦这两次栈存在差异(即非稳态阻塞),开发者便难以构建完整的调用链路,导致无法精准定位阻塞源头——既无法确认为何卡住、谁在阻塞,也无法锁定具体占用主线程的关键函数,难以定位问题根因。

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

AppFreeze增强采样栈,让你的鸿蒙应用“卡住”无处遁形-华为开发者话题 | 华为开发者联盟

 

用户在使用应用时,如果出现点击无反应或应用无响应等情况,并且持续时间超过一定限制,就会被定义为 AppFreeze (应用冻屏) ,即应用无响应。系统会检测应用无响应,并生成AppFreeze日志,供应用开发者分析。在利用AppFreeze日志分析冻屏问题时,传统排查手段往往受限于日志信息的匮乏:当接收到THREAD_BLOCK_6S故障时,系统仅能提供两次主线程调用快照。一旦这两次存在差异(即非稳态阻塞),开发者便难以构建完整的调用链路,导致无法精准定位阻塞源头——既无法确认为何卡住、谁在阻塞,也无法锁定具体占用主线程的关键函数,难以定位问题根因。

HarmonyOS从 API Version 21 开始AppFreeze 引入了增强日志信息(采样。通过采集整机及主线程的运行负载,并抓取多份主线程调用,对冻屏问题分析提供了有力的帮助。

一、核心升级:多次采样主线程信息

       AppFreeze 增强日志的诞生,改变了分析冻屏问题的方式。相比原有日志,它主要解决了两大痛点:

1. 精准定位故障期间的“主线程热点”

       过去,开发者针对两次堆栈不一致场景只能猜测问题出在哪。现在,增强日志会在冻屏发生时,自动抓取 1~10 次主线程调用。这就像给主线程拍了一组高速连拍照片,让你能清晰看到阻塞期间线程到底停在了哪个函数上,是死锁?是长耗时计算?还是系统调用?

2. 引入资源层面的“深度体检”

       除了堆栈,日志还包含了CPU 总体耗时信息。通过 SupplyAvailableTime(调度可优化时间)等字段,开发者可以判断主线程是否因为系统调度策略而被迫等待,还是真的在执行繁重任务。

二、工作原理:智能触发,自动采集

       这套机制非常聪明,它不会无脑采集,而是根据故障等级动态触发:

       1. 启动采集:当检测到 THREAD_BLOCK_3S 或 LIFECYCLE_HALF_TIMEOUT 时,系统立即开启主线程调用栈采集流程,记录当前 CPU 信息

   2. 停止并输出:当故障进一步升级为 THREAD_BLOCK_6SLIFECYCLE_TIMEOUT 或 APP_INPUT_BLOCK 时,停止采集,计算周期内的 CPU 信息,并生成包含多帧堆栈的增强日志

       小贴士:如果应用已接入 MAIN_THREAD_JANK 并自定义了堆栈采集个数,AppFreeze 的增强日志会自动对齐该配置,保持数据一致性

三、获取指南:两种方式,轻松获取

       开发者无需修改核心逻辑,即可通过以下两种便捷方式获取增强日志:

方式一:通过 HiAppEvent 订阅(推荐

       开发者可通过 HiAppEvent 订阅监听 APP_FREEZE 事件,其中 external_log 字段包含冻屏日志路径

默认方式

版本

说明

OpenHarmony 6.1.0.125 之前版本

冻屏日志文件仅包含普通冻屏故障信息

OpenHarmony 6.1.0.125 之后版本

系统将冻屏日志与增强日志合并为一份文件,内容分为:普通冻屏故障信息 + 增强日志信息

       快速定位增强日志:

       在 external_log 返回的 APP_FREEZE 文件中搜索 FREEZE_EXT_INFO 关键字,即可快速跳转到增强日志信息部分

独立获取

       开发者也可通过在 AppScope/app.json5 中配置环境变量,独立获取增强日志文件

"appEnvironments": [
  {
    "name""DFX_APPFREEZE_LOG_OPTIONS",
    "value""mainthread_sampling:enable"
  }
]

工程示例

       通过 external_log 字段读取故障日志和增强日志。external_log 是字符串数组

• 第一个元素:应用冻屏事件生成的故障文件路径

• 第二个元素:应用冻屏事件生成的增强日志文件路径

       开发者可通过 DevEco Studio 的 Log -> HiLog 页面查看 HiAppEvent 结果

方式二:通过 hdc 导出

       在开启开发者选项的情况下,可直接通过命令行导出:

hdc file recv /data/log/faultlog/freeze_ext D:\

       导出文件命名格式为:freeze-cpuinfoext-进程名-进程UID-毫秒级时间,方便归档分析。

       导出示例:

四、日志解读:如何利用采样信息

       AppFreeze 增强采样的核心结构如下:

1. 基础信息 (Basic Concepts)

       日志开头会详细列出统计周期内的 CPU 时间分布

• StaticsDuration总持续时间

• CpuTime: 主线程实际运行时间。

• SyncWaitTime主线程等待时间(睡眠+就绪)。

• OptimalCpuTime :统计周期内,主线程最优负载运行时间(使用最大核的最高算力运行)。

• SupplyAvailableTime调度可优化时间。如果值较小,说明主线程繁忙,需要开发者考虑优化主线程的任务

2. 多帧堆栈 (Snapshot)

       这是核心的部分。日志会包含多个 SnapshotTime 的调用,让你看到阻塞期间线程的动作序列:

#ThreadInfos Tid2204Name: com.example.freeze
SnapshotTime:2021-01-01-20-05-58.292875
#00 pc 00000000000015b8 [shmm]
#01 pc 00000000001d7e44 /system/lib64/ld-musl-aarck64.so.1(clock_gettime+48)
#02 pc 00000000001d9f20 /system/lib64/ld-musl-aarck64.so.1(time+32)
#03 pc 0000000000007e2c .../libsample.so(WaitSomeTime()+76)    ← 你的业务代码!
#04 pc 0000000000009b2c .../libsample.so
#05 pc 00000000000a0500 .../libruntime.z.so

       通过对比多帧堆栈,开发者可以排查线程是否卡死在某个循环或 I/O 操作上。

3. 任务提交者 (SubmitterStacktrace)

       如果是因为异步任务提交导致的主线程阻塞,这里会记录谁发起了这个任务(能精确到 ArkTS 源码行号),帮你定位到业务代码的调用源头。

========SubmitterStacktrace========
#00 pc 0000000000013108 /system/lib64/platformsdk/libuv.so(uv_queue_work+292)
#01 pc 0000000000008cdc .../libsample.so
#02 pc 000000000005ae00 .../libace_napi.z.so(ArkNativeFunctionCallBack+272)
#03 pc 0000000000de3efc .../arkcompiler/stub.an(RTStub_PushCallArgsAndDispatchNative+44)
#04 pc 0000000000448dd4 .../arkcompiler/stub.an(BCStub_HandleCallthis0Imm8V8StwCopy+372)
#05 at anonymous (sample|sample|1.0.0|src/main/ets/pages/Index.ets:381:36)  ← 元凶在此!

五、实战演练

       理论讲再多,不如实战来一次。下面通过一个典型的鸿蒙应用性能故障案例,展示如何利用 AppFreeze 增强日志(采样)快速定位根因。

案例:循环调用系统音频服务接口导致主线程繁忙

问题现象
当用户触发某项业务操作后,应用界面出现短暂卡顿,随后应用无响应并退出。系统生成了冻屏日志,需要从中定位根因。

问题分析

1. 初步排查:常规日志无法确因
首先查看 AppFreeze 中的 THREAD_BLOCK_3S 和 THREAD_BLOCK_6S 两部分的主线程堆栈

• 证据1:对比两次堆栈,发现它们不一致

      ■ THREAD_BLOCK_3S部分

Tid:11480Name:pfreezeanalysis
state=S, utime=507, stime=48, priority=-54, nice=-10, clk=100
#00 pc 000000000001c51c /system/lib64/libhilog_inner.so(OHOS::HiviewDFX::GetGlobalLevel() (.cfi)+92)(6354f07a703d8f245d82889f0cf8c14d)
#01 pc 00000000000185f0 /system/lib64/libhilog_inner.so(OHOS::HiviewDFX::HiLogPrintDictArgs(HilogMsg&, unsigned int, char const*, FmtId const*, char const*, std::__va_list) (.cfi)+416)(6354f07a703d8f245d82889f0cf8c14d)
#02 pc 0000000000003658 /system/lib64/platformsdk/libhilog.so(HiLogPrintDictNew.cfi+156)(e4cc09aac1ffa99c34fa89a22026469f)
#03 pc 000000000012dd74 /system/lib64/platformsdk/libappexecfwk_core.z.so(OHOS::AppExecFwk::BundleMgrProxy::GetBundleNameForUid(int, std::__h::basic_string<char, std::__h::char_traits<char>, std::__h::allocator<char>>&)+752)(80db8096f4ef7d86a16204925176d700)
#04 pc 00000000000848ac /system/lib64/platformsdk/libaudio_common.z.so(OHOS::AudioStandard::AppBundleManager::GetBundleNameFromUid(int) (.cfi)+600)(3d96f3d82968d2646a0b1ebdd75aa0b5)
#05 pc 000000000003b698 /system/lib64/platformsdk/libaudio_policy_manager.z.so(OHOS::AudioStandard::AudioSystemClientPolicyManager::GetAudioScene() const+72)(1c4bbc1b1972c3dc93180d60180cacc3)
#06 pc 00000000000fc598 /system/lib64/module/multimedia/libaudio.z.so(OHOS::AudioStandard::NapiAudioManager::GetAudioSceneSync(napi_env__*, napi_callback_info__*) (.cfi)+160)(afd152a27fc802f4dc979e8a5d253f7d)
#07 pc 000000000004f9b0 /system/lib64/platformsdk/libace_napi.z.so(panda::JSValueRef ArkNativeFunctionCallBack<true>(panda::JsiRuntimeCallInfo*)+288)(2bd324bcfae36fa9497b62ed308cf824)
#08 pc 0000000000e949e8 /system/lib64/module/arkcompiler/stub.an(RTStub_PushCallArgsAndDispatchNative+40)
#09 pc 00000000005a4658 /system/lib64/module/arkcompiler/stub.an(BCStub_HandleCallthis0withnameImm8Id16V8StwCopy+400)
#10 at triggerFrequentBinderCalls (entry|entry|1.0.0|src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ts:65:47)

      ■ THREAD_BLOCK_6S部分

Tid:11480Name:pfreezeanalysis
state=S, utime=570, stime=127, priority=-54, nice=-10, clk=100
#00 pc 000000000019c674 /system/lib/ld-musl-aarch64.so.1(ioctl+184)(58b5bc0b32f5d8462c0e616fbc5ee53a)
#01 pc 00000000000102e8 /system/lib64/platformsdk/libipc_common.z.so(OHOS::BinderConnector::WriteBinder(unsigned long, void*)+108)(79d15a46e94dbb151b338b9b4d955e09)
#02 pc 00000000000779e0 /system/lib64/platformsdk/libipc_single.z.so(OHOS::BinderInvoker::TransactWithDriver(bool)+284)(fd75264c3da5d3ad49399e757c1fcd72)
#03 pc 00000000000762ec /system/lib64/platformsdk/libipc_single.z.so(OHOS::BinderInvoker::WaitForCompletion(OHOS::MessageParcel*)+124)(fd75264c3da5d3ad49399e757c1fcd72)
#04 pc 000000000007587c /system/lib64/platformsdk/libipc_single.z.so(OHOS::BinderInvoker::SendRequest(int, unsigned int, OHOS::MessageParcel&, OHOS::MessageParcel&, OHOS::MessageOption&)+612)(fd75264c3da5d3ad49399e757c1fcd72)
#05 pc 000000000004cae4 /system/lib64/platformsdk/libipc_single.z.so(OHOS::IPCObjectProxy::SendRequestInner(bool, unsigned int, OHOS::MessageParcel&, OHOS::MessageParcel&, OHOS::MessageOption&)+148)(fd75264c3da5d3ad49399e757c1fcd72)
#06 pc 000000000004d4a0 /system/lib64/platformsdk/libipc_single.z.so(OHOS::IPCObjectProxy::SendRequest(unsigned int, OHOS::MessageParcel&, OHOS::MessageParcel&, OHOS::MessageOption&)+216)(fd75264c3da5d3ad49399e757c1fcd72)
#07 pc 00000000000b8cc0 /system/lib64/libaudio_framework_interface.z.so(OHOS::AudioStandard::AudioPolicyProxy::GetAudioScene(int&)+320)(541767ac76398d730304e9d891eb9543)
#08 pc 000000000003b67c /system/lib64/platformsdk/libaudio_policy_manager.z.so(OHOS::AudioStandard::AudioSystemClientPolicyManager::GetAudioScene() const+44)(1c4bbc1b1972c3dc93180d60180cacc3)
#09 pc 00000000000fc598 /system/lib64/module/multimedia/libaudio.z.so(OHOS::AudioStandard::NapiAudioManager::GetAudioSceneSync(napi_env__*, napi_callback_info__*) (.cfi)+160)(afd152a27fc802f4dc979e8a5d253f7d)
#10 pc 000000000004f9b0 /system/lib64/platformsdk/libace_napi.z.so(panda::JSValueRef ArkNativeFunctionCallBack<true>(panda::JsiRuntimeCallInfo*)+288)(2bd324bcfae36fa9497b62ed308cf824)
#11 pc 0000000000e949e8 /system/lib64/module/arkcompiler/stub.an(RTStub_PushCallArgsAndDispatchNative+40)
#12 pc 00000000005a4658 /system/lib64/module/arkcompiler/stub.an(BCStub_HandleCallthis0withnameImm8Id16V8StwCopy+400)
#13 at triggerFrequentBinderCalls (entry|entry|1.0.0|src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ts:65:47)

• THREAD_BLOCK_3S 堆栈中,主线程处于 S (Sleep/Ready) 状态,正在执行日志打印及音频上下文获取

• THREAD_BLOCK_6S 堆栈中,主线程进入了 Binder 通信等待阶段 (BinderInvoker::WaitForCompletion)。

       结论:两次堆栈不一致,说明主线程并非处于死锁或完全静止状态,而是处于繁忙执行中。此时仅靠快照无法确定具体耗时长的环节,必须引入增强日志中的多帧采样进行统计。

2. 深入挖掘:利用增强日志看热点
开启增强日志后,查看采样频率最高的关键栈帧(Top Hotspot)。

  • 证据2:增强日志中占比最高的帧序列如下:

#00 pc 000000000019c674 /system/lib/ld-musl-aarch64.so.1(ioctl+184)
#01 pc 00000000000102e8 /system/lib64/platformsdk/libipc_common.z.so(BinderConnector::WriteBinder)
...
#06 pc 000000000004d4a0 /system/lib64/platformsdk/libipc_single.z.so(IPCObjectProxy::SendRequest)
#07 pc 0000000000158dec .../libappexecfwk_core.z.so(BundleMgrProxy::SendTransactCmd)
#08 pc 000000000012dc04 .../libappexecfwk_core.z.so(BundleMgrProxy::GetBundleNameForUid)
#09 pc 00000000000848ac .../libaudio_common.z.so(AudioBundleManager::GetBundleNameFromUid)
#10 pc 000000000003b698 .../libaudio_policy_manager.z.so(AudioSystemClientPolicyManager::GetAudioScene)
#11 pc 00000000000fc598 .../libaudio.z.so(NapiAudioManager::GetAudioSceneSync)
#12 ... (ArkNativeFunctionCallBack)
#13 at triggerFrequentBinderCalls (entry|entry|1.0.0|src/main/ets/pages/.../appfreeze_threadblock.ts:65:47)

      分析

• 栈顶大量出现 Binder 相关的 IPC 通信函数,暗示主线程在频繁进行跨进程调用。

• 业务层明确指向 triggerFrequentBinderCalls 函数(位于 appfreeze_threadblock.ts 第 65 行)。

• 结合 GetAudioSceneSync 等音频相关接口,推测是在循环中频繁调用音频状态接口。

3. 锁定根因:查看业务代码

1. 证据3:定位到 triggerFrequentBinderCalls 源码

// 错误示范:在循环或高频触发事件中同步调用系统服务
function triggerFrequentBinderCalls() {
  let startTime = Date.now();
  let countnumber = 0;
  while (Date.now() - startTime <= PRE_CALL_DELAY_MS) {
    count++;
  }
  for (let i = 0; i < BINDER_CALL_COUNT; i++) {
    try {
      let audioManager = audio.getAudioManager();
      let value: audio.AudioScene = audioManager.getAudioSceneSync();
    } catch (err) {
      let code = (err as BusinessError).code;
      let message = (err as BusinessError).message;
      console.error(`error: ${code}${message}`);
    }
  }
}

• 问题

       1. 在循环中频繁调用 getAudioSceneSync(同步接口

       2. 每次循环都获取新的 audioManager 实例

       3. 同步调用会阻塞主线程

解决方案

• 优化方案

       1. 在应用启动或页面加载时一次性获取音频场景并缓存

       2. 使用异步接口替代同步接口(如果可能

       3. 减少不必要的重复调用,将 audioManager 实例化提到循环外

• 代码修正

// 正确做法:缓存结果
class AudioSceneManager {
  private static instanceAudioSceneManager;
  private cachedAudioScene: audio.AudioScene | null = null;
  private audioManager: audio.AudioManager;
  private constructor() {
    this.audioManager = audio.getAudioManager();
    this.cachedAudioScene = this.audioManager.getAudioSceneSync();
  }
  public static getInstance(): AudioSceneManager {
    if (!AudioSceneManager.instance) {
      AudioSceneManager.instance = new AudioSceneManager();
    }
    return AudioSceneManager.instance;
  }
  public getAudioScene(): audio.AudioScene {
    return this.cachedAudioScene!;
  }
  public refreshAudioScene(): void {
    this.cachedAudioScene = this.audioManager.getAudioSceneSync();
  }
}
function triggerFrequentBinderCalls() {
  const audioSceneManager = AudioSceneManager.getInstance();
  const audioScene = audioSceneManager.getAudioScene();
}

• 优化点

优化项

原代码问题

优化后

缓存策略

每次循环都调用同步接口

单例模式,初始化时获取一次

实例获取

循环内重复获取 audioManager

构造时获取一次,复用

同步调用

循环内多次阻塞主线程

仅初始化时调用一次

六、总结:建立科学的冻屏分析 SOP

       AppFreeze 增强日志的引入,标志着鸿蒙应用性能调优进入精准诊断阶段。建议开发者遵循以下 SOP 进行排查:

1.  看差异:对比 THREAD_BLOCK_3S 和 6S 堆栈

一致 → 可能是死锁或长期休眠 → 检查 SyncWaitTime

不一致 → 可能是繁忙或动态变化 → 进入下一步。

2.  找热点:打开增强日志,统计多帧堆栈中出现频率高的业务层函数(.ets/.ts/.js 文件)。

3. 看资源:结合 CpuTime 和 SupplyAvailableTime 判断是忙(计算密集)还是等(调度密集/资源竞争)。

4. 查代码:定位到具体文件行号,审查是否存在循环调用、同步 I/O、复杂计算等阻塞主线程的逻辑。

通过系统级的多帧采样和 CPU 资源分析,将模糊的卡顿还原为清晰的调用链。善用 AppFreeze 增强日志,

让性能优化有据可依,打造丝般顺滑的用户体验。

———————————————————————————————————-

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

🔗 社区DFX专题文章:  https://developer.huawei.com/consumer/cn/forum/subject/2101218731402391001

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

本文由 @华为开发者联盟 授权发布于人人都是产品经理。未经作者许可,禁止转载

题图来自Unsplash,基于CC0协议

该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
评论
评论请登录
  1. 目前还没评论,等你发挥!