RNOH 资源泄漏 DFX 分析指导文档
鸿蒙 DFX 资源泄漏(Resource Leak)检测能力用于监控 React Native for OpenHarmony(RNOH)框架及其接入应用运行期的资源未释放问题。资源泄漏分为句柄泄漏(FD_LEAK)、线程泄漏(THREAD_LEAK)、Native 对象泄漏和 JS 资源泄漏四类。本文聚焦 RNOH 场景下高频的句柄泄漏、线程泄漏与 Native 对象泄漏,JS 堆内存泄漏详见《内存泄漏 DFX 分析指导》。
当 socket/open/pipe/dmabuf 等系统句柄创建后未对应 close,或 Worker/std::thread 创建后未正确终止,或 TurboModule/ComponentInstance 对象销毁后残留引用未释放时,系统通过 LeakDetector 模块周期采样(60s)检测,句柄超 5000 个 或线程超 700 个 时抓取详细维测日志并上报泄漏事件,HarmonyOS 6.1.0+ 触发内核管控时可能查杀进程。
RNOH 的原生引擎层由 C++ 实现,核心 SO 包括 librnoh_core.so、librnoh_core_package.so、libreactnative.so、libjsvmtooling.so(JSVM 引擎)和 libhermestooling.so(Hermes 引擎)。资源泄漏问题在 RNOH 场景中高发于TurboModule 析构时回调未清理、HTTP 请求回调未释放、Worker 线程未终止、napi_ref 未释放、组件实例销毁后残留引用、事件监听器未注销等路径,历史修复记录覆盖 0.72 / 0.77 / 0.82 多个版本。
1 机制描述
1.1 鸿蒙系统资源泄漏检测机制
鸿蒙 DFX 资源泄漏检测由系统 LeakDetector 模块 主导,对进程资源使用周期采样(60s),超阈值时自动抓取维测并上报:
| 泄漏类型 | 检测机制 | 默认阈值 |
|---|---|---|
| 句柄泄漏(FD_LEAK) | 每 60s 遍历进程,获取 fd 句柄总数,超阈值抓详细句柄信息 + 上报 | 5000 个 |
| 线程泄漏(THREAD_LEAK) | 每 60s 遍历进程,获取总线程数,超阈值抓详细线程名信息 + 上报 | 700 个 |
阈值可通过 hidebug.setAppResourceLimit 接口自定义(建议仅开发阶段使用):
import { hidebug } from '@kit.PerformanceAnalysisKit';
// 开发调试示例:降低阈值以便快速复现
hidebug.setAppResourceLimit('fd', 100, true);
hidebug.setAppResourceLimit('thread', 50, true);
1.2 RNOH 资源类型与生命周期
RNOH 框架采用 C++ + ArkTS + JS 三层架构,涉及以下资源类型:
系统句柄(FD)资源
| 资源类型 | RNOH 场景来源 |
|---|---|
| socket fd | NetworkingTurboModule 的 HTTP 请求、WebSocket 连接(Inspector)、图片下载 |
| 文件 fd | JS Bundle 加载(OH_ResourceManager_OpenRawFile)、图片缓存文件读写 |
| ashmem fd | 图片共享内存、PixelMap 数据传递 |
| pipe fd | TaskExecutor 线程间通信 |
| dmabuf fd | 图形渲染 Buffer(DMA_HEAP_IOCTL_ALLOC) |
| sync_file fd | 图形同步 fence |
线程资源
| 资源类型 | RNOH 场景来源 |
|---|---|
| RNOH_JS 线程 | ThreadTaskRunner("RNOH_JS"),运行 JS 引擎和 JS 回调 |
| Worker 线程 | WorkerThread.ts 创建的 worker.ThreadWorker,用于 TurboModule 子线程 |
| RNOH_CLEANUP 线程 | ThreadTaskRunner("RNOH_CLEANUP"),用于实例清理 |
| 定时器线程 | uv::Timer(EventLoopTaskRunner)和 setInterval(ArkTS 侧) |
Native 对象资源
| 资源类型 | RNOH 场景来源 |
|---|---|
| TurboModule 实例 | ArkTSTurboModule 析构时需释放 CallbackGuard、注销全局注册表、释放 NapiRef |
| ComponentInstance | CppComponentInstance 子类析构时需释放 ArkUI 节点、注销 UITicker 监听 |
| napi_ref | 跨 JS/C++ 边界的引用包装,需通过 NapiRef RAII 或手动 napi_delete_reference 释放 |
| ImageCache 对象 | ImageCacheManagerInternal 析构时需 Shutdown dispatcher、释放 loader 和 download provider |
| JSVM_Script | JS 代码缓存句柄,需通过 OH_JSVM_ReleaseCache 释放(部分平台不支持,为已知泄漏) |
1.3 RNOH 资源管理机制
RNOH 框架中已实现的资源管理机制:
NapiRef RAII 包装(ArkJS.h):napi_ref 包装在 shared_ptr<napi_ref__> 中,自定义 deleter 调用 napi_delete_reference。当最后一个 shared_ptr 副本销毁时自动释放引用。
CallbackGuard / gcReleaseTrigger(ArkTSTurboModule.cpp):ArkTS 回调包装器通过 CallbackGuard 管理生命周期。gcReleaseTrigger 是一个带自定义 deleter 的 shared_ptr<void>,当 JS 回调被 GC 回收但未调用时,deleter 触发 CallbackGuard::release(),防止未调用回调的泄漏。
subscribe-returns-unsubscribe 模式(UITicker.h、EventEmitter.ts):事件订阅返回一个 std::function<void()> 或 () => void,调用后注销监听。RNInstanceInternal 析构时调用 m_unsubscribeUITickListener() 释放 VSync 监听。
unique_ptr + custom deleter 模式(JSBigStringHelpers.cpp、StyledStringWrapper.h):原生句柄通过 std::unique_ptr<RawFile, decltype(&OH_ResourceManager_CloseRawFile)> 管理,确保 RAII 释放。
1.4 日志获取
PID=$(hdc shell ps -ef | grep <包名> | awk '{print $2}' | head -1)
# 1. 句柄泄漏日志
hdc file recv /data/log/reliability/resource_leak/${PID}_fd_leak.txt ./
# 2. 线程泄漏日志
hdc file recv /data/log/reliability/resource_leak/${PID}_thread_leak.txt ./
# 3. HiAppEvent 订阅统一日志
hdc file recv /data/storage/el2/log/resourcelimit/ ./
# 4. 内核管控查杀日志(thread)
hdc file recv /data/log/reliability/resource_leak/threadleak-*-${PID}-log-*.txt ./
# 5. 进程整体内存信息(判断 Native 对象泄漏)
hdc shell hidumper --mem ${PID}
2 问题概述
2.1 常见泄漏类型与场景
| 泄漏类型 | 表现 | RNOH 高发路径 |
|---|---|---|
| socket fd 泄漏 | socket() 返回 fd 未 close,TCP 句柄累积 | NetworkingTurboModule HTTP 请求、Inspector WebSocket |
| 文件 fd 泄漏 | open() 返回 fd 未 close,文件句柄累积 | Bundle 加载、图片缓存文件读写 |
| ashmem fd 泄漏 | open("/dev/ashmem") 未 close,共享内存句柄累积 | PixelMap、图片共享内存 |
| pipe fd 泄漏 | pipe() 返回读端+写端 fd 均未 close | TaskExecutor 线程间通信 |
| dmabuf fd 泄漏 | ioctl(DMA_HEAP_IOCTL_ALLOC) 返回 fd 未 close | 图形渲染 Buffer |
| Worker 线程泄漏 | Worker.start() 未 requestTermination(),线程累积 | TurboModule 子线程、Worker RNInstance |
| TurboModule 对象泄漏 | TurboModule 析构时未释放回调和资源 | AppearanceTurboModule、NetworkingTurboModule |
| napi_ref 泄漏 | napi_ref 未通过 napi_delete_reference 释放 | ArkTSTurboModule 实例引用、Inspector WebSocket |
| 组件实例泄漏 | ComponentInstance 销毁后残留资源 | 图片组件、滚动组件 |
| JSVM 代码缓存泄漏 | JSVM_Script 句柄未释放 | JS 代码缓存路径(部分平台不支持释放,为已知泄漏) |
2.2 RNOH 资源泄漏背景
RNOH 框架涉及大量跨线程交互(JS 线程、UI 线程、Worker 线程)、异步回调(VSync、Timer、HTTP、NAPI)和复杂的对象生命周期管理(shared_ptr/weak_ptr、NAPI reference、JSVM 引用)。资源泄漏问题主要集中在以下路径:
- TurboModule 析构不完整:
ArkTSTurboModule析构时需执行CallbackGlobalRegistry::unregisterModule、CallbackGuard::releaseAll、NapiRef释放,任一步骤遗漏均导致泄漏。 - HTTP 请求回调未清理:请求结束后回调对象未释放,
NetworkingTurboModule析构时缺少资源释放逻辑。 - Worker 线程未终止:
WorkerThread.ts中setInterval在 ACK 未到达时不会清除;TurboModule 子线程退出依赖显式requestTermination。 - napi_ref 跨线程释放:
RNArkTSComponentDelegate使用原始napi_ref手动释放,若析构时 env 已失效则释放失败。 - TaskExecutor 清理风险:析构器将 task runners 移入 detached 线程,若 NAPI env 已销毁可能死锁或泄漏。
- 组件实例销毁后残留引用:
ComponentInstance基类析构器为= default,子类需自行释放资源。 - 事件监听器未注销:
on注册后缺少off对应,闭包持续持有上下文对象。
2.3 泄漏传导与识别机制
资源泄漏并非瞬时故障,而是「资源创建 → 未释放 → 系统 60s 采样命中阈值 → 抓维测+上报 → 内核管控查杀」的累积过程:
资源创建(socket / open / pipe / Worker.start / pthread_create / TurboModule 构造 / napi_create_reference)
↓
资源未释放(fd 丢弃 / Worker 永不退出 / 析构遗漏 / 监听器未注销)
↓
系统 LeakDetector 每 60s 周期采样 → fd ≥ 5000 或 线程 ≥ 700
↓
抓取详细维测日志([pid]_fd_leak.txt / [pid]_thread_leak.txt)+ HiAppEvent 上报
↓
HarmonyOS 6.1.0+ 内核管控查杀(ThreadKiller / 资源耗尽)
说明:系统检测是主动的(60s 采样 + 阈值判定),开发者侧是被动接收日志(HiAppEvent 订阅 / DevEco Testing 采集)。Native 对象泄漏不一定触发系统阈值,需结合
hidumper --mem和 DevEco Profiler 主动排查。
3 排查思路与整体分析
3.1 整体排查流程
资源泄漏排查遵循「先分类、再取证、后定位」的原则:
- 判断泄漏类型:通过
hidumper --mem查看进程整体内存,通过[pid]_fd_leak.txt查看句柄数,通过[pid]_thread_leak.txt查看线程数,区分是 FD 泄漏、线程泄漏还是 Native 对象泄漏。 - 获取维测日志:从设备拉取 LeakDetector 生成的泄漏日志和 HiAppEvent 上报事件。
- 分析日志聚类:对 FD 泄漏看 Top 10 句柄类型和目录聚类;对线程泄漏看 Top 10 线程名和启动时间;对 Native 对象泄漏看 DevEco Profiler 的 Allocation 视图。
- 符号化定位:对 FD 申请栈和线程快照栈做
addr2line符号化,还原到具体代码行。 - 交叉验证:结合代码审查资源创建与销毁的对账关系,确认泄漏源。
3.2 工具链
| 工具 | 用途 | 适用泄漏类型 |
|---|---|---|
| hidumper --mem | 查看进程整体 Pss 和 Native Heap | Native 对象泄漏 |
| [pid]_fd_leak.txt | 句柄泄漏维测日志 | FD 泄漏 |
| [pid]_thread_leak.txt | 线程泄漏维测日志 | 线程泄漏 |
| addr2line | FD 申请栈和线程栈符号化 | FD 泄漏、线程泄漏 |
| DevEco Studio Profiler | Native 内存分配和释放分析 | Native 对象泄漏 |
| React Native DevTools | JS 侧对象存活检查(辅助定位 napi_ref 泄漏) | JS 资源泄漏 |
| HiAppEvent 订阅 | 接收 RESOURCE_OVERLIMIT 事件 | 所有类型 |
3.3 工具规格约束
- 开发态分析需使用真机或模拟器连接进行。
nolog版本默认不抓 FD 申请栈,需在「开发者选项」打开「系统资源泄漏日志」并重启设备使能抓栈功能。- LeakDetector 阈值为系统级配置,开发阶段可通过
hidebug.setAppResourceLimit降低阈值以便快速复现。 - 运维态的内存泄漏问题分析当前不支持。
4 分步排查过程(工具使用)
4.1 第一步:复现问题场景
操作流程:
- 启动 RNOH 应用。
- 进入目标页面/功能。
- 执行触发泄漏的操作(如:频繁打开关闭页面、反复发起 HTTP 请求、反复创建销毁 TurboModule)。
- 返回首页或退出页面(模拟资源释放)。
- 等待 60s 以上(LeakDetector 采样周期),或手动触发
hidebug.setAppResourceLimit降低阈值加速复现。
4.2 第二步:判断泄漏类型
使用 hidumper 命令查看进程整体内存:
hdc shell hidumper --mem $(pidof com.example.rnoh)
- Pss 持续增长且不回落:存在内存类泄漏(Native 对象泄漏或 JS 内存泄漏)。
- fd 数量持续增长:存在句柄泄漏,需拉取
[pid]_fd_leak.txt。 - 线程数持续增长:存在线程泄漏,需拉取
[pid]_thread_leak.txt。
注意:最后一次操作后等待 2min 或手动触发 GC 后再次抓取数据,避免瞬时资源未释放导致的误判。
4.3 分析句柄泄漏日志
句柄泄漏定位四步流程:① 看总量和增长模式 → ② 看 Leaked fd Top 10 确定 FD 类型 → ③ 看 Dir Type Top 10 确定文件路径 → ④ 分析 FD 申请栈 addr2line 反解定位代码行。
4.3.1 看总量和增长模式
time: 2026/07/30 20:00:06
pid: 14144
process: com.example.rnoh
leaked fd nums: 10587
先区分增长模式:
| 增长模式 | 判断 |
|---|---|
| 单次业务后增加,业务结束后回落 | 可能是正常瞬时使用 |
| 首次使用后增加,后续保持稳定 | 可能是连接池或缓存 |
| 每执行一次业务永久增加固定数量 | 强泄漏信号 |
| 并发期间快速增加,空闲后不下降 | 重点检查异常和超时路径 |
4.3.2 看 Leaked fd Top 10
*****************************
Summary:
Leaked fd:socket
Leaked fd Top 10:
10508 socket
10 eventfd
9 pipe
7 eventpoll
6 ashmem
5 dmabuf
4 sync_file
根据第一名确定排查方向:
| FD 类型 | RNOH 场景优先检查 |
|---|---|
| socket | NetworkingTurboModule HTTP 请求、Inspector WebSocket 是否在失败/超时/重连路径 close |
| 普通文件路径 | OH_ResourceManager_OpenRawFile/open 是否和 OH_ResourceManager_CloseRawFile/close 配对 |
| pipe | TaskExecutor 线程间通信的 pipe 读写两端是否全部关闭 |
| ashmem | PixelMap、图片共享内存对象及 FD 是否释放 |
| dmabuf/ION | 图形、渲染 Buffer 是否 release/destroy |
| sync_file | fence FD 是否关闭 |
| .so 路径 | dlopen 是否缺少 dlclose,或对象长期不析构 |
特殊类型句柄维测:当 Top 1 属于 ashmem/socket/pipe/sync_file/dmabuf 五类且该类型 > 1000 个,日志增加整机详细维测:
| 特殊类型 | 关键字段 | 定位作用 |
|---|---|---|
| ashmem | Process_name / Fd / Applicant_Pid / Ashmem_name / Size | 共享内存申请来源 |
| socket | ProcessName / Fd / inode / PeerTid | 网络连接对端 |
| pipe | ProcessName / Fd / PipeName / MaxUsage / RingSize | 管道名+使用量 |
| sync_file | ProcessName / Fd / FenceName / DriverName / Status | fence 驱动+状态 |
| dmabuf | Process name / Fd / size / magic / buf->pid / buf->task_comm | buffer 申请人(magic 相同=同一 buffer) |
4.3.3 看 Dir Type Top 10
Dir Type Top 10:
6175 /data/storage/el2/database/rdb ← db 文件句柄泄漏
5 /data/storage/el1/bundle ← HAP 资源文件句柄
3 /sys/kernel/debug/tracing/trace_marker
| 目录模式 | RNOH 可能原因 |
|---|---|
/data/storage/el2/database/rdb | 数据库连接/结果集未关闭 |
/data/storage/el1/bundle | 重复打开 HAP、Bundle 文件或资源文件 |
/data/storage/el2/base/files | 图片缓存文件句柄未关闭 |
| 临时文件路径 | 图片下载临时文件生命周期过长 |
4.3.4 分析 FD 申请栈
系统 hook open/pipe 等系统调用约 10 分钟,按相同调用栈聚类:
*****************************
LOGGER_MEMCHECK_FD_STACK_INFO
pid: 12326
get stack time: 2024/06/17 19:16:48
==============================FdTrack Stack==============================
Generated by HiviewDFX @HarmonyOS
==============================Sorted by num==============================
num 8272 bt [/system/lib64/libfdleak_tracker.so+0x1fb58] [/system/lib/ld-musl-aarch64.so.1+0x1d3154] ...
num 3968 bt [/system/lib64/libfdleak_tracker.so+0x1fb58] [/system/lib64/platformsdk/libipc_core.z.so+0x4ac64] ...
判断规则:
num是该栈在 10 分钟采集窗口内申请 FD 的总次数;bt的调用顺序按官方说明为从右向左;- 忽略
libfdleak_tracker.so等维测框架栈; - 使用同一版本、同一 BuildId 的未剥离 SO 做
addr2line:
# 假设泄漏发生在 librnoh_core_package.so 的 0x195b68 处
addr2line -C -f -e /path/to/librnoh_core_package.so 0x195b68
- 找到第一个 RNOH 业务 SO 栈帧(
librnoh_core.so、librnoh_core_package.so、libreactnative.so),再向上追溯对象所有者。
官方限制:这里统计的是 10 分钟内的全量申请栈,没有剔除后来已经
close的句柄。因此高频栈只是"重点嫌疑",不能单凭num判定泄漏;必须与检测时仍存活的 FD 类型、目录和业务执行次数交叉验证。
4.4 分析线程泄漏日志
线程泄漏维测日志文件名 [pid]_thread_leak.txt,按以下五步分析:
4.4.1 看总量和伴随现象
time: 2026/07/30 19:38:46
pid: 14144
vss: 213601988
rss: 1057144
process: com.example.rnoh
summary: 2849
summary 是检测时进程线程总数。线程泄漏往往伴随:
| 伴随现象 | 原因 |
|---|---|
| VSS 持续增大 | 每个线程需要栈地址空间 |
| RSS 增长 | 取决于线程栈实际使用页数 |
| CPU 增长 | 取决于泄漏线程是在等待还是忙循环 |
最终 pthread_create/任务调度失败 | 系统线程资源耗尽 |
4.4.2 看 Top 10 Thread Name
Top 10 Thread Name:
2818 com.example.rnoh
5 OS_GC_Thread
3 RNOH_JS
2 RNOH_CLEANUP
1 OS_DfxWatchdog
判断方法:
- 某个业务线程名数量异常集中 → 优先排查该组件;
RNOH_JS或RNOH_CLEANUP数量异常 → 检查 RNInstance 创建与销毁是否对账;- 两类线程总是以相同数量增长 → 通常来自同一个上层对象或 SDK 实例。
4.4.3 结合线程启动时间
======================================================
tid thread_name start_time(jiffies)
221 RNOH_JS 4688297
240 IPC_3_4318 3081382
把启动时间与这些事件关联:
- 页面进入/退出
- RNInstance 创建/销毁
- TurboModule 反复初始化
- 前后台切换
- Worker/Native 模块重新加载
4.4.4 分析全量线程快照
======================================================
Tid:17033, Name:com.example.rnoh
state=S, utime=1, stime=0, priority=10, nice=-10, clk=100
#00 pc 00000000001e3900 /system/lib/ld-musl-aarch64.so.1(__timedwait_cp+192)
#01 pc 00000000001e5a5c /system/lib/ld-musl-aarch64.so.1(pthread_cond_timedwait+188)
#02 pc 000000000291ddfc /data/storage/el1/bundle/libs/arm64/librnoh_core_package.so(...)
需要继续追问:
- 谁创建了该线程?
- 为什么对象销毁后线程仍在等待?
- 是否设置退出标记?
- 设置退出标记后是否唤醒等待线程?
- 是否执行
join? - 是否只是
detach后永久循环?
线程快照是「线程当前在哪里」,通常不是「线程当初在哪里创建」。
4.4.5 检查创建与退出协议
| 创建方式 | 正确退出动作 | RNOH 场景 |
|---|---|---|
std::thread | 通知退出 → join();仅 detach() 不会停止线程 | TaskExecutor 的 ThreadTaskRunner |
| ArkTS ThreadWorker | 移除监听 → terminate() | WorkerThread.ts、TurboModule 子线程 |
pthread_create | 设置停止标志 → 唤醒阻塞 → 线程函数返回 → pthread_join | EventLoopTaskRunner |
| TurboModule 实例 | 调用析构 → 释放 CallbackGuard → 释放 NapiRef | ArkTSTurboModule::~ArkTSTurboModule() |
| 监听器内部线程 | off/unregister 后销毁所属对象 | UITicker subscribe/unsubscribe |
两个常见误区:
pthread_join不是停止线程:如果线程卡在无限循环或永久等待,必须先发退出信号并唤醒,再join。pthread_detach只表示线程结束后自动回收线程控制资源:无法回收一个永不退出的线程。
内核管控日志(HarmonyOS 6.1.0+):
Generated by HiviewDFX @HarmonyOS
LOGGER_MEMCHECK_GERNAL_INFO
pidNumber : 8387
processName : com.example.rnoh
killReason : ThreadKiller
threadCount : 3001
the process is killed by runnable thread threshold 3001 / 4124.
4.5 分析 Native 对象泄漏
当 hidumper --mem 显示 Pss 持续增长但 FD 和线程数未超阈值时,需使用 DevEco Studio Profiler 分析 Native 对象泄漏。
操作步骤:
- 在 DevEco Studio 中打开目标工程,连接设备后启动 Profiler。
- 选择 Allocation 功能,开始记录 Native 内存分配。
- 执行触发泄漏的操作(如反复进入退出页面、反复创建销毁 TurboModule)。
- 停止记录,查看 Allocation 视图。
- 分析分配栈(火焰图),定位是哪些 C++ 对象持续创建但没有释放。
- 重点关注以下对象类型:
ArkTSTurboModule子类实例CppComponentInstance子类实例NapiRef/napi_ref包装对象ImageCacheManagerInternal/ImageCacheDispatcher实例CallbackGuard/CallbackWrapper实例
4.6 辅助:使用 React Native DevTools 排查 JS 侧资源泄漏
当怀疑 napi_ref 泄漏导致 JS 对象无法被 GC 回收时,可使用 React Native DevTools 辅助排查。
操作步骤:
- 启动 React Native DevTools(
npx react-devtools+hdc rport tcp:8097 tcp:8097)。 - 进入 Memory 面板。
- 在触发泄漏操作前抓取一次 Hermes 堆快照。
- 执行触发泄漏的操作。
- 在操作后再次抓取堆快照。
- 对比两次快照,识别在操作后仍然存活的对象。
- 分析对象的引用链路,检查是否有 C++ 侧通过 napi_ref 持有的引用阻止了 JS 对象回收。
5 根因定位
根据泄漏类型的不同,根因定位的侧重点也不同。
5.1 句柄泄漏(FD_LEAK)
常见根因:
- NetworkingTurboModule 的 HTTP 请求在失败、超时、重连路径未
closesocket fd。 OH_ResourceManager_OpenRawFile打开的 Bundle 文件未通过OH_ResourceManager_CloseRawFile关闭。- 图片缓存文件读写使用的 fd 在异常路径未关闭。
pipe创建的读写两端未全部关闭。dlopen加载的 SO 缺少dlclose。
定位方法:
通过 [pid]_fd_leak.txt 的 Leaked fd Top 10 确定泄漏 FD 类型,通过 Dir Type Top 10 确定泄漏文件路径,通过 FD 申请栈 addr2line 还原到具体代码行。重点关注 RNOH 业务 SO 栈帧(librnoh_core_package.so、librnoh_core.so)。
5.2 线程泄漏(THREAD_LEAK)
常见根因:
WorkerThread.ts中setInterval在 ACK 未到达时不清除,导致定时器泄漏。- TurboModule 子线程缺少
requestTermination/terminate调用。 TaskExecutor析构器将 task runners 移入 detached 线程,线程可能永不退出。EventLoopTaskRunner的 libuv 事件循环未正确停止。RNInstance的onDestroy是async方法,若调用方未await或 Ability 被提前销毁,清理链中断。
定位方法:
通过 [pid]_thread_leak.txt 的 Top 10 Thread Name 确定泄漏线程类型,结合线程启动时间关联业务事件,通过线程快照栈分析线程当前状态(等待/忙循环),检查创建与退出协议是否配对。
5.3 TurboModule 对象泄漏
常见根因:
ArkTSTurboModule析构时未执行CallbackGlobalRegistry::unregisterModule。CallbackGuard::releaseAll在 JS runtime 已销毁时通过jsInvoker->invokeAsync投递但无法执行。arkTSTurboModuleInstanceRef(NapiRef)未在正确线程释放。- HTTP 请求回调在请求结束后未清理,TurboModule 析构时缺少资源释放逻辑。
历史修复参考:
| 版本 | 问题描述 | 提交 |
|---|---|---|
| 0.72.112 | HTTP 请求回调未清理,TurboModule 析构缺少资源释放 | 58179914c |
| 0.82.3 | TurboModule 与网络请求回调内存泄漏 | c56478622 |
定位方法:
通过 DevEco Studio Profiler 的 Allocation 功能,查看 ArkTSTurboModule 子类实例是否持续累积。结合代码审查 ~ArkTSTurboModule() 析构器是否完整执行了 unregisterModule → releaseAll → NapiRef 释放三步。
5.4 napi_ref 泄漏
常见根因:
RNArkTSComponentDelegate使用原始napi_ref手动释放,析构时 env 已失效导致释放失败。- Inspector WebSocket 的
NapiRef在 WebSocket 关闭后未释放。 - TurboModule Factory 持有的
arkTSTurboModuleProviderRef未在实例销毁时释放。
定位方法:
结合 DevEco Studio Profiler(Native 侧 napi_ref 包装对象累积)和 React Native DevTools(JS 侧对象无法 GC 回收)双重定位。JS 侧找到持续存活的对象,Native 侧找到未释放的 napi_ref 来源。
5.5 组件实例泄漏
常见根因:
ComponentInstance基类析构器为= default,子类未自行释放 ArkUI 节点和监听器。ComponentInstanceProvider析构时 UITicker 监听未注销(已有保护但存在重入风险)。ComponentManagerRegistry.releaseComponentManager(tag)未被显式调用。
历史修复参考:
| 版本 | 问题描述 | 提交 |
|---|---|---|
| 0.72.59 | CppComponentInstance 子类析构时资源未释放 | 4ae6d4561 |
5.6 JSVM 代码缓存泄漏
常见根因:
JSVM_Script句柄在部分平台(NEXT-DB3)无法通过OH_JSVM_ReleaseCache释放,为已知泄漏。
历史修复参考:
| 版本 | 问题描述 | 提交 |
|---|---|---|
| 0.77.40 | JSVM 代码缓存创建后句柄未释放 | e6a993196 |
6 解决方案
6.1 句柄泄漏修复
- socket fd:
socket()返回 fd 必须在用完后close(fd),即使连接失败也要 close;用try/finally包裹保证异常路径也释放;connect不可达时设SO_SNDTIMEO/SO_RCVTIMEO,超时后 close。 - 文件 fd:
OH_ResourceManager_OpenRawFile必须与OH_ResourceManager_CloseRawFile配对,推荐使用std::unique_ptr<RawFile, decltype(&OH_ResourceManager_CloseRawFile)>RAII 包装。 - pipe fd:
pipe()返回两个 fd,读端 + 写端都要 close,用try/finally保证两端都释放。 - 设备 fd(ashmem/dmabuf/sync_file):用完立即 close;
ioctl分配出的新 fd 须单独管理生命周期,勿与设备 fd 混淆。
6.2 线程泄漏修复
- Worker 线程:每个
Worker.start()必须有requestTermination();存入容器的 Worker 任务完成后终止;为WorkerThread.ts的setInterval增加超时保护,ACK 未到达时清除定时器并报错。 - std::thread:通知退出 → 唤醒阻塞 → 线程函数返回 →
join();仅detach()不会停止线程。 - TaskExecutor:修复 detached 线程清理逻辑,确保 NAPI env 销毁前完成 task runner 清理。
- EventLoopTaskRunner:确保
cleanup()正确执行,m_isAlive=false→m_running=false→ 重置m_asyncHandle→ 清除队列和定时器。
6.3 TurboModule 对象泄漏修复
- 确保
~ArkTSTurboModule()完整执行三步清理:CallbackGlobalRegistry::unregisterModule(name_, m_callbackGuardRegistry.get())— 注销全局注册表。m_callbackGuardRegistry->releaseAll()— 释放所有回调(若 JS runtime 已销毁则同步释放并捕获异常)。taskExecutor->runTask(m_ctx.turboModuleThread, [ref = std::move(m_ctx.arkTSTurboModuleInstanceRef)] {})— 在正确线程释放 NapiRef。
- 为 HTTP 请求回调补充完成后的清理逻辑(参考
HttpRequestHelper.ts的cleanUp模式:cleanedUp防重入保护 +.off()注销所有.on()handler +httpRequest.destroy())。 - 利用
gcReleaseTrigger机制确保未调用回调的自动释放。
6.4 napi_ref 泄漏修复
- 优先使用
NapiRefRAII 包装(ArkJS.h)替代原始napi_ref,确保shared_ptr销毁时自动调用napi_delete_reference。 - 必须使用原始
napi_ref时(如RNArkTSComponentDelegate),在析构器中增加 env 有效性检查,env 失效时记录日志而非静默失败。 - 确保 NapiRef 在正确的线程释放(TurboModule 线程或 JS 线程),避免跨线程释放。
6.5 组件实例泄漏修复
ComponentInstance子类必须实现析构器,释放 ArkUI 节点、注销监听器、清理子组件引用。- 确保
ComponentInstanceProvider析构时正确调用m_unsubscribeUITickListener()。 - 在 ETS 侧确保
ComponentManagerRegistry.releaseComponentManager(tag)在组件销毁时被显式调用。 - 使用
DescriptorRegistry.destroy()的cleanUpCallbacks模式确保所有订阅被注销。
6.6 通用修复原则
- 建立资源创建与销毁的对账关系,对 TurboModule、ComponentInstance、ImageCache、Worker 逐项检查释放闭环。
- 所有
on/subscribe/register都必须有对应的off/unsubscribe/unregister。 - 在同步等待路径中避免因为线程阻塞而跳过清理流程。
- 对长生命周期模块做资源计数回归检查(参考
ImageCacheMetrics::Instance().Objects()的计数模式)。 RNInstance.onDestroy()的async调用方必须await,避免清理链中断。
6.7 修复验证
完成修复后,按以下步骤验证:
- 复现原泄漏场景(如反复触发 10 次句柄 / 7 次线程),确认系统 60s 采样后不再生成
[pid]_fd_leak.txt/[pid]_thread_leak.txt。 hdc shell ls /data/log/reliability/resource_leak/确认无新泄漏日志。- HiAppEvent 订阅确认不再收到
RESOURCE_OVERLIMIT事件。 - 若已触发内核管控,确认无新
threadleak-*-log-*.txt查杀日志。 - 长时压测(如反复进出页面 100 次)确认
leaked fd nums与summary稳定不累积,未超 5000/700 阈值。 hidumper --mem确认 Pss 内存稳定不累积。
7 经验总结
综合 RNOH 历史修复记录和代码库资源管理模式,可以提炼出以下高频判断规律:
- 先分类再定位:不要急于猜测根因,先用
hidumper和 LeakDetector 日志判断是 FD 泄漏、线程泄漏还是 Native 对象泄漏,再选择对应工具。 - FD 泄漏看 Top 10 聚类:Leaked fd Top 10 确定 FD 类型(socket/文件/ashmem/pipe),Dir Type Top 10 确定泄漏路径,FD 申请栈
addr2line定位代码行。三者交叉验证才能确认根因。 - 线程泄漏看线程名和状态:Top 10 Thread Name 定位泄漏组件,线程快照看当前状态(等待/忙循环),启动时间关联业务事件。
pthread_join不是停止线程,必须先唤醒再 join。 - TurboModule 析构三步不能少:unregisterModule → releaseAll → NapiRef 释放。任一步骤遗漏或执行顺序错误均导致泄漏。JS runtime 已销毁时需走同步 fallback 路径并捕获异常。
- napi_ref 优先用 RAII:
NapiRef的shared_ptr+ 自定义 deleter 是最安全的模式。必须用原始napi_ref时需增加 env 有效性检查。 - 异步清理链不能中断:
RNInstance.onDestroy()是async,调用方必须await。for...of中某个 surface stop 失败不应跳过后续清理。 - 订阅必须有反订阅:
on/subscribe/register必须有对应的off/unsubscribe/unregister。subscribe-returns-unsubscribe模式是最安全的做法。 - **FD 申请栈是"嫌疑"不是"定罪"**:10 分钟采集窗口内的全量申请栈没有剔除已 close 的句柄。高频栈只是重点嫌疑,必须与检测时仍存活的 FD 类型和业务执行次数交叉验证。
- detached 线程是高风险:
TaskExecutor析构器将 task runners 移入 detached 线程,若 NAPI env 已销毁可能死锁或泄漏。必须在 env 销毁前完成清理。 - 善用 RAII 和智能指针:
unique_ptr+ custom deleter、shared_ptr+ 自定义 deleter、NapiRef包装器是防止资源泄漏的最有效手段。
附录
附录 A:hidumper 与 LeakDetector 命令参考
# 查看指定进程的内存信息
hdc shell hidumper --mem $(pidof com.example.rnoh)
# 查看进程 fd 数量
hdc shell ls /proc/$(pidof com.example.rnoh)/fd | wc -l
# 查看进程线程数量
hdc shell ls /proc/$(pidof com.example.rnoh)/task | wc -l
# 拉取句柄泄漏日志
hdc file recv /data/log/reliability/resource_leak/${PID}_fd_leak.txt ./
# 拉取线程泄漏日志
hdc file recv /data/log/reliability/resource_leak/${PID}_thread_leak.txt ./
# 拉取 HiAppEvent 统一日志
hdc file recv /data/storage/el2/log/resourcelimit/ ./
# 拉取内核管控查杀日志
hdc file recv /data/log/reliability/resource_leak/threadleak-*-${PID}-log-*.txt ./
附录 B:LeakDetector 阈值配置
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| fd | int | 5000 | 句柄泄漏检测阈值,超阈值抓详细维测并上报 |
| thread | int | 700 | 线程泄漏检测阈值,超阈值抓详细维测并上报 |
import { hidebug } from '@kit.PerformanceAnalysisKit';
// 开发阶段降低阈值以便快速复现
hidebug.setAppResourceLimit('fd', 100, true);
hidebug.setAppResourceLimit('thread', 50, true);
附录 C:RNOH 资源管理机制源码索引
| 机制 | 关键文件 |
|---|---|
| NapiRef RAII 包装 | src/main/cpp/RNOH/ArkJS.h(NapiRef + Deleter) |
| CallbackGuard / gcReleaseTrigger | src/main/cpp/RNOH/ArkTSTurboModule.cpp(~ArkTSTurboModule、CallbackGuard、gcReleaseTrigger) |
| subscribe-returns-unsubscribe | src/main/cpp/RNOH/UITicker.h(subscribe 返回 unsubscribe callable) |
| unique_ptr + custom deleter | src/main/cpp/RNOH/JSBigStringHelpers.cpp(OH_ResourceManager_CloseRawFile) |
| Timer 管理与清理 | src/main/cpp/RNOH/HarmonyTimerRegistry.cpp(~HarmonyTimerRegistry 取消 wake-up task) |
| ImageCache 析构与 Shutdown | src/main/cpp/RNOH/ImageCache/src/ImageCacheManagerInternal.cpp(~ImageCacheManagerInternal) |
| Worker 线程管理 | src/main/ets/RNOH/WorkerThread.ts(create / destroy) |
| TaskExecutor 清理 | src/main/cpp/RNOH/TaskExecutor/TaskExecutor.cpp(~TaskExecutor + detached 线程) |
| EventLoopTaskRunner 清理 | src/main/cpp/RNOH/TaskExecutor/EventLoopTaskRunner.cpp(cleanup + m_timerByTaskId 清理) |
| RNInstance 销毁 | src/main/ets/RNOH/RNInstance.ts(onDestroy)、src/main/cpp/RNOH/RNInstanceInternal.cpp(~RNInstanceInternal) |
| ComponentInstance 生命周期 | src/main/cpp/RNOH/ComponentInstance.h(~ComponentInstance)、src/main/cpp/RNOH/ComponentInstanceProvider.cpp(~ComponentInstanceProvider) |
| HTTP 请求回调清理 | src/main/ets/RNOH/HttpRequestHelper.ts(cleanUp 模式) |
| ImageCacheMetrics 计数 | src/main/cpp/RNOH/ImageCache/include/ImageCacheMetrics.h(Objects().Increment/Decrement) |
附录 D:历史资源泄漏修复记录
| 版本 | 问题描述 | 影响模块 | 提交 |
|---|---|---|---|
| 0.72.59 | CppComponentInstance 子类析构时资源未释放 | ComponentInstance | 4ae6d4561 |
| 0.72.112 | HTTP 请求回调未清理,TurboModule 析构缺少资源释放 | NetworkingTurboModule / AppearanceTurboModule | 58179914c |
| 0.72.128 | runOnQueueSync 在 JS 线程上的任务闭包未释放 | EventLoopTaskRunner / ThreadTaskRunner | 781706f14 |
| 0.77.40 | JSVM 代码缓存创建后句柄未释放 | JSVMRuntime | e6a993196 |
| 0.82.3 | TurboModule 与网络请求回调内存泄漏 | NetworkingTurboModule / AppearanceTurboModule | c56478622 |
附录 E:相关文档
- 内存泄漏 DFX 分析指导 — RN 内存泄漏问题的机制说明、hidumper 判定、JS/Native 侧定位工具与自动检测机制
- RNOH 踩内存 DFX 分析指导文档 — ASan/HWASan/GWP-ASan 检测器原理与使用,覆盖 UAF、越界等内存误用问题
- 华为:句柄泄漏分析方法 — 鸿蒙系统句柄泄漏分析的官方最佳实践
- 稳定性概览 — RNOH 应用稳定性问题分类概览
- 稳定性分析方法 — RNOH 应用稳定性问题分析方法论
- 稳定性案例 — RNOH 应用稳定性问题典型案例分析
更多推荐

所有评论(0)