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

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:
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 导致的瞬间卡顿。
———————————————————————————————————-
🔗 官网开发者学堂视频: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协议
该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务
- 目前还没评论,等你发挥!

起点课堂会员权益



