鸿蒙 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 fdNetworkingTurboModule 的 HTTP 请求、WebSocket 连接(Inspector)、图片下载
文件 fdJS Bundle 加载(OH_ResourceManager_OpenRawFile)、图片缓存文件读写
ashmem fd图片共享内存、PixelMap 数据传递
pipe fdTaskExecutor 线程间通信
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
ComponentInstanceCppComponentInstance 子类析构时需释放 ArkUI 节点、注销 UITicker 监听
napi_ref跨 JS/C++ 边界的引用包装,需通过 NapiRef RAII 或手动 napi_delete_reference 释放
ImageCache 对象ImageCacheManagerInternal 析构时需 Shutdown dispatcher、释放 loader 和 download provider
JSVM_ScriptJS 代码缓存句柄,需通过 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 均未 closeTaskExecutor 线程间通信
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 整体排查流程

资源泄漏排查遵循「先分类、再取证、后定位」的原则:

  1. 判断泄漏类型:通过 hidumper --mem 查看进程整体内存,通过 [pid]_fd_leak.txt 查看句柄数,通过 [pid]_thread_leak.txt 查看线程数,区分是 FD 泄漏、线程泄漏还是 Native 对象泄漏。
  2. 获取维测日志:从设备拉取 LeakDetector 生成的泄漏日志和 HiAppEvent 上报事件。
  3. 分析日志聚类:对 FD 泄漏看 Top 10 句柄类型和目录聚类;对线程泄漏看 Top 10 线程名和启动时间;对 Native 对象泄漏看 DevEco Profiler 的 Allocation 视图。
  4. 符号化定位:对 FD 申请栈和线程快照栈做 addr2line 符号化,还原到具体代码行。
  5. 交叉验证:结合代码审查资源创建与销毁的对账关系,确认泄漏源。

3.2 工具链

工具用途适用泄漏类型
hidumper --mem查看进程整体 Pss 和 Native HeapNative 对象泄漏
[pid]_fd_leak.txt句柄泄漏维测日志FD 泄漏
[pid]_thread_leak.txt线程泄漏维测日志线程泄漏
addr2lineFD 申请栈和线程栈符号化FD 泄漏、线程泄漏
DevEco Studio ProfilerNative 内存分配和释放分析Native 对象泄漏
React Native DevToolsJS 侧对象存活检查(辅助定位 napi_ref 泄漏)JS 资源泄漏
HiAppEvent 订阅接收 RESOURCE_OVERLIMIT 事件所有类型

3.3 工具规格约束

  • 开发态分析需使用真机或模拟器连接进行。
  • nolog 版本默认不抓 FD 申请栈,需在「开发者选项」打开「系统资源泄漏日志」并重启设备使能抓栈功能。
  • LeakDetector 阈值为系统级配置,开发阶段可通过 hidebug.setAppResourceLimit 降低阈值以便快速复现。
  • 运维态的内存泄漏问题分析当前不支持。

4 分步排查过程(工具使用)

4.1 第一步:复现问题场景

操作流程:

  1. 启动 RNOH 应用。
  2. 进入目标页面/功能。
  3. 执行触发泄漏的操作(如:频繁打开关闭页面、反复发起 HTTP 请求、反复创建销毁 TurboModule)。
  4. 返回首页或退出页面(模拟资源释放)。
  5. 等待 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 场景优先检查
socketNetworkingTurboModule HTTP 请求、Inspector WebSocket 是否在失败/超时/重连路径 close
普通文件路径OH_ResourceManager_OpenRawFile/open 是否和 OH_ResourceManager_CloseRawFile/close 配对
pipeTaskExecutor 线程间通信的 pipe 读写两端是否全部关闭
ashmemPixelMap、图片共享内存对象及 FD 是否释放
dmabuf/ION图形、渲染 Buffer 是否 release/destroy
sync_filefence FD 是否关闭
.so 路径dlopen 是否缺少 dlclose,或对象长期不析构

特殊类型句柄维测:当 Top 1 属于 ashmem/socket/pipe/sync_file/dmabuf 五类且该类型 > 1000 个,日志增加整机详细维测:

特殊类型关键字段定位作用
ashmemProcess_name / Fd / Applicant_Pid / Ashmem_name / Size共享内存申请来源
socketProcessName / Fd / inode / PeerTid网络连接对端
pipeProcessName / Fd / PipeName / MaxUsage / RingSize管道名+使用量
sync_fileProcessName / Fd / FenceName / DriverName / Statusfence 驱动+状态
dmabufProcess name / Fd / size / magic / buf->pid / buf->task_commbuffer 申请人(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_joinEventLoopTaskRunner
TurboModule 实例调用析构 → 释放 CallbackGuard → 释放 NapiRefArkTSTurboModule::~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 对象泄漏。

操作步骤:

  1. 在 DevEco Studio 中打开目标工程,连接设备后启动 Profiler。
  2. 选择 Allocation 功能,开始记录 Native 内存分配。
  3. 执行触发泄漏的操作(如反复进入退出页面、反复创建销毁 TurboModule)。
  4. 停止记录,查看 Allocation 视图。
  5. 分析分配栈(火焰图),定位是哪些 C++ 对象持续创建但没有释放。
  6. 重点关注以下对象类型:
    • ArkTSTurboModule 子类实例
    • CppComponentInstance 子类实例
    • NapiRef / napi_ref 包装对象
    • ImageCacheManagerInternal / ImageCacheDispatcher 实例
    • CallbackGuard / CallbackWrapper 实例

4.6 辅助:使用 React Native DevTools 排查 JS 侧资源泄漏

当怀疑 napi_ref 泄漏导致 JS 对象无法被 GC 回收时,可使用 React Native DevTools 辅助排查。

操作步骤:

  1. 启动 React Native DevTools(npx react-devtools + hdc rport tcp:8097 tcp:8097)。
  2. 进入 Memory 面板。
  3. 在触发泄漏操作前抓取一次 Hermes 堆快照。
  4. 执行触发泄漏的操作。
  5. 在操作后再次抓取堆快照。
  6. 对比两次快照,识别在操作后仍然存活的对象。
  7. 分析对象的引用链路,检查是否有 C++ 侧通过 napi_ref 持有的引用阻止了 JS 对象回收。

5 根因定位

根据泄漏类型的不同,根因定位的侧重点也不同。

5.1 句柄泄漏(FD_LEAK)

常见根因:

  • NetworkingTurboModule 的 HTTP 请求在失败、超时、重连路径未 close socket 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.112HTTP 请求回调未清理,TurboModule 析构缺少资源释放58179914c
0.82.3TurboModule 与网络请求回调内存泄漏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.59CppComponentInstance 子类析构时资源未释放4ae6d4561

5.6 JSVM 代码缓存泄漏

常见根因:

  • JSVM_Script 句柄在部分平台(NEXT-DB3)无法通过 OH_JSVM_ReleaseCache 释放,为已知泄漏。

历史修复参考:

版本问题描述提交
0.77.40JSVM 代码缓存创建后句柄未释放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() 完整执行三步清理:
    1. CallbackGlobalRegistry::unregisterModule(name_, m_callbackGuardRegistry.get()) — 注销全局注册表。
    2. m_callbackGuardRegistry->releaseAll() — 释放所有回调(若 JS runtime 已销毁则同步释放并捕获异常)。
    3. 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 泄漏修复

  • 优先使用 NapiRef RAII 包装(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 修复验证

完成修复后,按以下步骤验证:

  1. 复现原泄漏场景(如反复触发 10 次句柄 / 7 次线程),确认系统 60s 采样后不再生成 [pid]_fd_leak.txt / [pid]_thread_leak.txt。
  2. hdc shell ls /data/log/reliability/resource_leak/ 确认无新泄漏日志。
  3. HiAppEvent 订阅确认不再收到 RESOURCE_OVERLIMIT 事件。
  4. 若已触发内核管控,确认无新 threadleak-*-log-*.txt 查杀日志。
  5. 长时压测(如反复进出页面 100 次)确认 leaked fd nums 与 summary 稳定不累积,未超 5000/700 阈值。
  6. hidumper --mem 确认 Pss 内存稳定不累积。

7 经验总结

综合 RNOH 历史修复记录和代码库资源管理模式,可以提炼出以下高频判断规律:

  1. 先分类再定位:不要急于猜测根因,先用 hidumper 和 LeakDetector 日志判断是 FD 泄漏、线程泄漏还是 Native 对象泄漏,再选择对应工具。
  2. FD 泄漏看 Top 10 聚类:Leaked fd Top 10 确定 FD 类型(socket/文件/ashmem/pipe),Dir Type Top 10 确定泄漏路径,FD 申请栈 addr2line 定位代码行。三者交叉验证才能确认根因。
  3. 线程泄漏看线程名和状态:Top 10 Thread Name 定位泄漏组件,线程快照看当前状态(等待/忙循环),启动时间关联业务事件。pthread_join 不是停止线程,必须先唤醒再 join。
  4. TurboModule 析构三步不能少:unregisterModule → releaseAll → NapiRef 释放。任一步骤遗漏或执行顺序错误均导致泄漏。JS runtime 已销毁时需走同步 fallback 路径并捕获异常。
  5. napi_ref 优先用 RAII:NapiRef 的 shared_ptr + 自定义 deleter 是最安全的模式。必须用原始 napi_ref 时需增加 env 有效性检查。
  6. 异步清理链不能中断:RNInstance.onDestroy() 是 async,调用方必须 await。for...of 中某个 surface stop 失败不应跳过后续清理。
  7. 订阅必须有反订阅:on/subscribe/register 必须有对应的 off/unsubscribe/unregister。subscribe-returns-unsubscribe 模式是最安全的做法。
  8. **FD 申请栈是"嫌疑"不是"定罪"**:10 分钟采集窗口内的全量申请栈没有剔除已 close 的句柄。高频栈只是重点嫌疑,必须与检测时仍存活的 FD 类型和业务执行次数交叉验证。
  9. detached 线程是高风险:TaskExecutor 析构器将 task runners 移入 detached 线程,若 NAPI env 已销毁可能死锁或泄漏。必须在 env 销毁前完成清理。
  10. 善用 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 阈值配置

参数类型默认值说明
fdint5000句柄泄漏检测阈值,超阈值抓详细维测并上报
threadint700线程泄漏检测阈值,超阈值抓详细维测并上报
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 / gcReleaseTriggersrc/main/cpp/RNOH/ArkTSTurboModule.cpp(~ArkTSTurboModule、CallbackGuard、gcReleaseTrigger)
subscribe-returns-unsubscribesrc/main/cpp/RNOH/UITicker.h(subscribe 返回 unsubscribe callable)
unique_ptr + custom deletersrc/main/cpp/RNOH/JSBigStringHelpers.cpp(OH_ResourceManager_CloseRawFile)
Timer 管理与清理src/main/cpp/RNOH/HarmonyTimerRegistry.cpp(~HarmonyTimerRegistry 取消 wake-up task)
ImageCache 析构与 Shutdownsrc/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.59CppComponentInstance 子类析构时资源未释放ComponentInstance4ae6d4561
0.72.112HTTP 请求回调未清理,TurboModule 析构缺少资源释放NetworkingTurboModule / AppearanceTurboModule58179914c
0.72.128runOnQueueSync 在 JS 线程上的任务闭包未释放EventLoopTaskRunner / ThreadTaskRunner781706f14
0.77.40JSVM 代码缓存创建后句柄未释放JSVMRuntimee6a993196
0.82.3TurboModule 与网络请求回调内存泄漏NetworkingTurboModule / AppearanceTurboModulec56478622

附录 E:相关文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐