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

AppFreeze采样栈定位“IO阻塞”,解决鸿蒙应用中同步读写主线程超时问题-华为开发者话题 | 华为开发者联盟

前面分享了“锁竞争”和“UI 渲染过载”导致冻屏的问题,这一篇将探讨“IO 阻塞”引发的冻屏问题。在鸿蒙应用开发中,如果在主线程执行同步文件读写操作,可能会阻塞主线程,导致应用卡顿甚至被系统杀死。本文将分析如何利用采样栈分析同步 IO 阻塞造成的AppFreeze问题。


1. 问题现象:从“卡顿”到“冻屏”

       用户触发某项涉及文件处理或数据同步的业务操作后,应用界面出现明显卡顿,随后进入无响应状态,最终被系统强制退出。

关键线索:我们需要从系统生成的 AppFreeze 日志中,找出导致主线程超时的根因。

2. 问题分析:从快照到多次采样的深度挖掘

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

       查看 AppFreeze 日志中的 THREAD_BLOCK_3S 和 THREAD_BLOCK_6S 主线程堆栈快照,两次堆栈不一致。

       THREAD_BLOCK_3S 快照片段

Tid:45449, Name:pfreezeanalysis
state=R, utime=419, stime=23, priority=-54, nice=-10, clk=100
#00 pc 0000000000623aec /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::JSThread::Iterate(panda::ecmascript::RootVisitor&, panda::ecmascript::GlobalVisitType)+524)(528445894c7c84ccb26a5e5f1d8f925b)
#01 pc 00000000006d4c84 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::Marker::MarkRoots(panda::ecmascript::RootVisitor&, panda::ecmascript::GlobalVisitType)+128)(528445894c7c84ccb26a5e5f1d8f925b)
#02 pc 0000000000668380 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::ConcurrentMarker::InitializeMarking()+280)(528445894c7c84ccb26a5e5f1d8f925b)
#03 pc 0000000000667ef8 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::ConcurrentMarker::Mark()+1840)(528445894c7c84ccb26a5e5f1d8f925b)
#04 pc 000000000068c1fc /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::Heap::TryTriggerConcurrentMarking(panda::ecmascript::MarkReason)+1020)(528445894c7c84ccb26a5e5f1d8f925b)
#05 pc 000000000069657c /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::LinearSpace::Allocate(unsigned long, bool)+176)(528445894c7c84ccb26a5e5f1d8f925b)
#06 pc 000000000080b078 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::ObjectFactory::NewTaggedArray(unsigned int, panda::ecmascript::JSTaggedValue)+800)(528445894c7c84ccb26a5e5f1d8f925b)
#07 pc 00000000005019d0 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::RuntimeStubs::RuntimeGetCallSpreadArgs(panda::ecmascript::JSThread*, panda::ecmascript::JSHandle<panda::ecmascript::JSTaggedValue> const&)+92)(528445894c7c84ccb26a5e5f1d8f925b)
#08 pc 00000000008f9ea4 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::RuntimeStubs::CallSpread(unsigned long, unsigned int, unsigned long)+344)(528445894c7c84ccb26a5e5f1d8f925b)
#09 pc 0000000000e92148 /system/lib64/module/arkcompiler/stub.an(RTStub_CallRuntime+40)
#10 pc 000000000054b190 /system/lib64/module/arkcompiler/stub.an(BCStub_HandleApplyImm8V8V8StwCopy+72)
#11 at anonymous (/usr1/hmos_for_system/src/increment/sourcecode/out/generic_generic_arm_64only/general_all_phone_standard_2d/obj/commonlibrary/ets_utils/js_api_module/buffer/js_buffer.js:253:1)
#12 at triggerHighFrequentIO (entry|entry|1.0.0|src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ts:21:17)

       THREAD_BLOCK_6S 快照片段

Tid:45449, Name:pfreezeanalysis
state=R, utime=677, stime=57, priority=-54, nice=-10, clk=100
#00 pc 00000000001f474c /system/lib/ld-musl-aarch64.so.1(write+64)(58b5bc0b32f5d8462c0e616fbc5ee53a)
#01 pc 000000000001c408 /system/lib64/platformsdk/libuv.so(uv__fs_work+2564)(5c9b6391b25c71005320ce5b0e68e238)
#02 pc 000000000001ec1c /system/lib64/platformsdk/libuv.so(uv_fs_write+300)(5c9b6391b25c71005320ce5b0e68e238)
#03 pc 0000000000160e2c /system/lib64/module/file/libfs.z.so(OHOS::FileManagement::ModuleFileIO::PropNExporter::WriteSync(napi_env__*, napi_callback_info__*) (.cfi)+896)(e3a582b785def20ae95665a025945421)
#04 pc 000000000004f9b0 /system/lib64/platformsdk/libace_napi.z.so(panda::JSValueRef ArkNativeFunctionCallBack<true>(panda::JsiRuntimeCallInfo*)+288)(2bd324bcfae36fa9497b62ed308cf824)
#05 pc 0000000000e949e8 /system/lib64/module/arkcompiler/stub.an(RTStub_PushCallArgsAndDispatchNative+40)
#06 pc 00000000005a4e24 /system/lib64/module/arkcompiler/stub.an(BCStub_HandleCallthis2withnameImm8Id16V8V8V8StwCopy+440)
#07 at highFrequencyIO5 (entry|entry|1.0.0|src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ts:47:8)
#08 at triggerHighFrequentIO (entry|entry|1.0.0|src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ts:25:9)

       证据 1:两次堆栈不一致 —— 揭示“动态过程”

时间点

线程状态

关键调用栈 (Top Frames)

行为分析

3s 快照

R (Running)

JSRuntime 内部内存分配 (Allocate)、垃圾回收标记 (MarkRoots)

预备阶段:主线程正在为即将进行的操作准备内存或执行 JS 运行时相关的准备工作。

6s 快照

R (Running)

ld-musl-aarch64.so.1(write) → libuv.so → libfs.z.so(WriteSync)

执行阶段:主线程正在执行同步的文件写入操作,且耗时较长。

       深度解析

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

  • 堆栈不一致:3秒时在准备内存/上下文,6秒时在进行实际的系统调用(Write)。这表明主线程在执行一个耗时的同步操作

 • 局限性:常规快照只能看到“点”。虽然 6s 快照指向了 write 系统调用,但无法确认这是偶发的一次写入,还是高频循环导致的累计耗时。必须引入增强日志中的多帧采样统计来确认“热点”。

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

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

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

#00 pc 00000000001f474c /system/lib/ld-musl-aarch64.so.1(write+64)(58b5bc0b32f5d8462c0e616fbc5ee53a)
#01 pc 000000000001c408 /system/lib64/platformsdk/libuv.so(uv__fs_work+2564)(5c9b6391b25c71005320ce5b0e68e238)
#02 pc 000000000001ec1c /system/lib64/platformsdk/libuv.so(uv_fs_write+300)(5c9b6391b25c71005320ce5b0e68e238)
#03 pc 0000000000160e2c /system/lib64/module/file/libfs.z.so(OHOS::FileManagement::ModuleFileIO::PropNExporter::WriteSync(napi_env__*, napi_callback_info__*) (.cfi)+896)(e3a582b785def20ae95665a025945421)
#04 pc 000000000004f9b0 /system/lib64/platformsdk/libace_napi.z.so(panda::JSValueRef ArkNativeFunctionCallBack<true>(panda::JsiRuntimeCallInfo*)+288)(2bd324bcfae36fa9497b62ed308cf824)
#05 pc 0000000000e949e8 /system/lib64/module/arkcompiler/stub.an(RTStub_PushCallArgsAndDispatchNative+40)
#06 pc 00000000005a4e24 /system/lib64/module/arkcompiler/stub.an(BCStub_HandleCallthis2withnameImm8Id16V8V8V8StwCopy+440)
#07 at highFrequencyIO5 (entry|entry|1.0.0|src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ts:47:8)
#08 at triggerHighFrequentIO (entry|entry|1.0.0|src/main/ets/pages/page_second/page_third_appfreeze/appfreeze_threadblock.ts:25:9)

       关键线索提取:

   • I/O 操作明确:栈顶 write 以及 libuv 线程池工作函数,明确表明主线程正在执行阻塞式文件 I/O

   • 业务层定位:明确指向 appfreeze_threadblock.ts 的 triggerHighFrequentIO 函数。

   • 高频特征:由于这是增强日志中采样次数最多的栈帧,暗示该 I/O 操作被反复执行,导致主线程长时间无法释放。

       💡 推测:业务代码可能在循环中调用了同步的文件读写接口(如 writeSync),导致主线程被阻塞在高频率的磁盘 I/O 上。

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

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

       问题代码片段

function triggerHighFrequentIO() {
  let startTime = Date.now();
  let count: number = 0;
  // 模拟耗时等待
  while (Date.now() - startTime <= PRE_IO_DELAY_MS) {
    count++;
  }
  try {
    const file = fileIo.openSync(
      `${getContext().filesDir}/freeze_test.dat`,
      fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC
    );
    // 问题点:在循环中连续进行同步 I/O 操作
    for (let iterationIndex = 0; iterationIndex < IO_OPERATION_COUNT; iterationIndex++) {
      writeIO0Operation1(file);
      writeIO0Operation2(file);
      writeIO0Operation3(file);
      writeIO0Operation4(file);
      writeIO0Operation5(file);
    }
    fs.closeSync(file);
  } catch (err) {
    // 错误处理
  }
}

       根因总结:

   • 同步 I/O 阻塞主线程:writeSync 等同步接口会阻塞调用线程,直到 I/O 操作完成。

   • 高频循环加剧阻塞:在 for 循环中连续执行多次同步 I/O,导致主线程在极短时间内连续阻塞,累计耗时超过系统冻屏阈值(通常为 3s 或 6s)。

   • 缺乏异步机制:未使用 TaskPool 或 Worker 等异步机制将 I/O 操作卸载到后台线程,导致 UI 渲染线程被完全占用。

3. 解决方案与实践案例

优化策略

策略

说明

性能收益

异步 I/O 操作

使用 Promise 风格的异步 API(如 open、write)或 TaskPool/Worker。

释放主线程,避免阻塞 UI 渲染。

批量 I/O 操作

如果必须使用同步 I/O,应减少调用次数,例如将多次小写入合并为一次大写入。

减少系统调用次数,降低上下文切换开销。

避免主线程计算阻塞

确保主线程不包含耗时计算逻辑(如案例中的 while 循环)。

保证主线程快速响应用户交互。

优化后代码示例

// 定义异步 I/O 任务
async function performAsyncIO(file: fs.File, data: string): Promise<void> {
  return new Promise((resolve, reject) => {
    file.write(data, (err: BusinessError | null) => {
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
  });
}

// 使用 TaskPool 执行耗时的 I/O 密集型任务
async function triggerAsynchronousIO() {
  const data = "Sample Data for IO Operation";
  
  try {
    // 1. 异步打开文件
    const file = await new Promise<fs.File>((resolve, reject) => {
      fs.open({
        uri: `${getContext().filesDir}/freeze_test.dat`,
        flags: fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC,
        mode: 0o600
      }, (err: BusinessError | null, fd: fs.File) => {
        if (err) reject(err);
        else resolve(fd);
      });
    });

    // 2. 在 TaskPool 中执行批量 I/O 操作,避免阻塞主线程
    await taskpool.execute(async () => {
      for (let i = 0; i < 100; i++) { // 假设执行 100 次
        await performAsyncIO(file, `${data} - Batch ${i}\n`);
      }
    });

    // 3. 关闭文件
    await new Promise<void>((resolve, reject) => {
      file.close(() => resolve());
    });

  } catch (err) {
    console.error(`IO Operation failed: ${(err as BusinessError).message}`);
  }
}

4. 总结与启示

       通过这个案例,我们展示了如何利用 AppFreeze 增强日志精准定位主线程 I/O 阻塞问题:

   • 快照看状态:通过对比不同时间点的快照,判断线程是“阻塞”还是“繁忙”,并初步识别出系统调用类型(如 write)。

   • 采样看热点:通过增强日志的统计功能,快速定位高频执行的函数栈,确认是高频同步 I/O 导致了主线程繁忙。

   • 代码看逻辑:结合业务代码,发现“循环中同步 I/O”的性能陷阱。

   • 架构做优化:引入异步 I/O 和 TaskPool/Worker 机制,将耗时操作卸载到后台,从根本上解决主线程阻塞问题。

开发者贴士 Checklist

       遇到主线程繁忙/冻屏问题时,请按以下步骤排查:

1. 检查增强日志中的 I/O 关键字

       在 AppFreeze 增强日志的主线程采样栈中,若栈顶共享库为 ld-musl-aarch64.so.1,请重点检查符号是否匹配以下 高频 I/O 阻塞关键字

 • 文件读写类

o write / writev:常见于同步文件写入。

o __stdio_write / __stdio_readx:常见于 C 标准库格式化读写。

o pread / pread64 / pwrite / pwrite64:常见于随机定位读写。

 • 动态加载类

o dlopen_impl:常见于动态加载共享库或模块。

       注意:如果上述符号在采样统计中频率极高,且伴随业务层代码调用(如 Sync 后缀 API),则极大概率为根因。

2. 常规代码自检

 • 禁用在主线程执行同步 I/O:检查是否有使用 openSync、writeSync、readSync 等接口,尤其是在循环中调用。

 • 检查耗时计算:确保主线程不包含复杂的 CPU 密集型计算或长耗时 while 循环。

 • 检查网络与数据库:确认是否有在主线程发起网络请求或执行未异步化的数据库操作。

3. 优化实践

 • 优先异步化:将文件 I/O、网络请求、数据库操作改为 Promise/async-await 风格。

 • 使用后台线程:对于批量或耗时较长的 I/O 任务,使用 TaskPool 或 Worker 进行卸载。

 • 减少系统调用次数:若必须使用同步 I/O,尽量合并数据,减少调用频次(如批量写入而非逐条写入)。

 • 模块预加载:避免在运行时动态加载未预加载的模块,防止 dlopen_impl 导致的瞬间卡顿。

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

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

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

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

Logo

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

更多推荐