本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:Local Handle与Global Handle内存分析实践-华为开发者话题 |华为开发者联盟
概述
在 HarmonyOS 的 ArkTS 与 C/C++跨语言交互场景中,应用频繁通过 Node-API 在 Native 层创建并持有 ArkTS 对象的引用句柄。当这些句柄的生命周期管理不当时,会引发 Native 侧的内存泄漏:Local Handle(对应 napi_value)未在作用域结束前正确释放、Global Handle(对应 napi_ref)强引用创建后未调用 napi_delete_reference 删除,都会使 ArkTS 对象被长期持有,垃圾回收器无法回收,从而造成内存持续增长。
然而,这类泄漏由于发生在 Native 侧,使用常规的 ArkTS 内存快照(rawheap)难以直接定位"哪一次 napi 句柄创建未释放",因为快照只能看到未被回收的对象,无法直接关联到创建该句柄的 Native 调用栈。
ArkTS 对象经 Node-API 被 Native 层长期持有造成的内存泄漏,通常会带来以下影响:
1. 性能:应用占用内存持续增长,系统为释放内存频繁触发 GC,GC 执行时会暂停应用主线程(Stop-The-World 机制),导致界面卡顿、滑动不流畅;长期泄漏也会使内存碎片化严重,分配/释放效率降低。
2. 内存:泄漏内存持续积累并达到 ArkTS 堆或进程 OOM 的上限阈值时,会产生 JS Crash。
3. 功耗:系统频繁 GC 消耗大量 CPU 资源,持续高占用会导致设备发热,加速电量消耗。
4. 功能:部分泄漏会因对象引用残留间接导致功能异常(如回调重复执行、状态错乱等)。
本文将介绍以下内容:
• Local Handle 与 Global Handle 简介
• 采集机制
• 生成数据说明
• ArkTS 堆快照聚类分析规则
• 命令行采集
• 场景案例
• 常见问题
实现原理
Local Handle 与 Global Handle 简介
HarmonyOS 通过 Node-API 在 Native 层操作 ArkTS 对象时,涉及两类引用句柄:
• Local Handle:用于管理 ArkTS 对象生命周期的引用句柄,对应 Node-API 中的 napi_value。napi_value 是一个表示 ArkTS 值的抽象类型,可表示基本类型(数字、字符串、布尔值)和复杂对象类型(数组、函数、对象等)。Node-API 通过 handle scope(句柄作用域)管理其生命周期:使用 napi_open_handle_scope 创建作用域,在作用域内创建的 napi_value 句柄会在 napi_close_handle_scope 关闭作用域时自动释放。框架层在执行开发者编写的 native 函数前会自动 open scope、函数结束后自动 close scope,因此定义在接口映射表中的函数无需手动管理作用域。若开发者在 native 侧脱离框架自动 scope 管理(如异步回调、长期存活的 native 对象中)持有 napi_value,或未正确关闭自行打开的作用域,句柄将无法被回收。
• Global Handle:用于跨作用域管理 ArkTS 值生命周期的引用句柄,对应 Node-API 中的 napi_ref。napi_ref 分为强引用和弱引用两种:弱引用创建时引用计数初始化为 0,不会阻止垃圾回收;强引用创建时引用计数初始化为 1(大于 0),会阻止垃圾回收器回收被引用的对象,必须手动调用 napi_delete_reference 释放,否则会导致内存泄漏。创建强引用 napi_ref 后忘记删除,或引用计数管理不当,会使 ArkTS 对象被 Native 层长期强引用,GC 无法回收。
说明:采集时不会抓取弱引用(引用计数为 0 的 napi_ref)的调用栈,因为它不阻止对象被 GC 回收,不构成泄漏。Local Handle 仅支持 Phone 和 PC 设备采集。
二者对比如下:
采集机制
Local Handle 与 Global Handle 采集能力由 HiProfiler 的 native hook 插件提供,通过 restrace_tag 参数指定要采集的资源类型。支持两种采集入口:
• DevEco Studio Profiler(Allocation 任务):在"All Heap & Anonymous VM"泳道的录制配置中,通过 Record Data Range Options(DevEco Studio 6.1.0 Release 新增)勾选 Local Handle 和 Global Handle,默认仅勾选 Malloc。展开 All Heap 泳道的 Native Heap 子泳道可分别查看 Malloc、ArkLocalHandle、ArkGlobalHandle 的内存分配。DevEco Studio Profiler 相关操作可参考:
• 命令行 hiprofiler_cmd:通过 restrace_tag 参数指定 RES_ARK_LOCAL_HANDLE 或 RES_ARK_GLOBAL_HANDLE,适合脚本化、长时间采集场景。本文实践 demo 以此方式为主。
native hook 插件通过 hook Ark 引擎中句柄的创建与销毁接口,记录每一次 Local Handle/Global Handle 创建的调用栈。结合分配与释放的匹配机制:在匹配间隔内分配并释放的调用栈不被记录,未被匹配释放的即为泄漏对象。对于 Local Handle,由于要求被测应用在启动时替换加载维测库,采集到的句柄记录均为未被回收的泄漏对象。
在 profiler 代码中,restrace 类型通过索引区分:
栈采集数据以 protobuf 格式写入 htrace 文件。API 26.0.0 起,统计模式新增 local/global handle 地址和 buildId 信息。
生成数据说明
trace 文件可通过 DevEco Studio Profiler 的离线导入功能进行解析,导入的单个文件大小不超过 1.5G。解析后 Native Heap 子泳道展示 ArkLocalHandle/ArkGlobalHandle 的分配统计(Statistics 标签页)、调用树(Call Trees 标签页)与分配列表(Allocations List 标签页)。

ArkTS 堆快照聚类分析规则
trace 文件(htrace)定位的是"哪个 Native 调用栈创建了未释放的句柄",而 ArkTS 堆快照(rawheap)记录的是所有无法被 GC 回收的 ArkTS 对象;两者配合使用——先用 trace 文件定位句柄创建栈,再结合 rawheap 从对象维度做聚类分析,可加速锁定泄漏对象。以下聚类规则针对 rawheap 快照分析。
通过 rawheap 定位内存泄漏时,快照中对象数量可达十万级以上,无法人工快速识别同类型不同业务对象各自的内存占用,需聚类规则指导分析。建议优先聚类 top20 目录下、retained size 占比 5%以上的对象。聚类规则主要有三种:
特殊对象无需或另行处理:SourceTextModule(每 ts 文件对应一个,天然聚类)、HiddenClass(与对象 1 对多,每个一类)、GlobalEnv/GlobalObject(快照内唯一)无需聚类;Promise/PromiseRecord 等异步对象按 PromiseReaction 下 handle 信息+最短引用链聚类;proxy 按 target 信息+引用链聚类。ArkTS 内存快照聚类分析规则详见ArkTS内存快照聚类分析规则
命令行采集
功能概述
ArkTS内存快照聚类分析规则借助 hiprofiler_cmd,您无需编写任何代码,只需在命令行中调整 Native Hook 插件配置参数,便能轻松为指定 Debug 签名应用快速开启 Local Handle/Global Handle 调用栈追踪功能。
使用方法
• 确认应用为可调试应用(使用调试证书签名):
以包名 com.example.myapplication 为例,执行:
hdc shell "bm dump -n com.example.myapplication | grep appProvisionType"
预期返回 "appProvisionType": "debug"。构建可调试应用需使用调试证书签名,申请调试证书可参考debug版本应用 。user 版本设备上的 release 签名应用不支持采集。
• 构建应用时保留符号表:参考模块级 build-profile.json5 文件,增加 strip 字段并赋值为 false,不移除.so 文件中的符号表、调试信息。采集到的函数栈在解析符号时需附带符号表信息,如无符号表则无法解析到正确的函数名。
• 确认设备已连接,hdc 环境已就绪。
规格说明
说明:若应用在生命周期内被强制终止后重启,再次录制 Local Handle 时仍会重启应用。
场景案例
场景描述
开发人员观测到应用进程内存持续增长,且应用中存在大量 Node-API 跨语言交互代码(如 C++侧缓存 ArkTS 对象、异步回调持有句柄等)。需定位是哪一次 napi_value 或 napi_ref 的创建未释放,并分析其引用关系与涉及代码行。
开发步骤
1. 抓取指定进程 Global Handle 对象的调用栈
从 API version 23 开始支持抓取指定进程创建 napi_ref 的调用栈,不会抓取创建弱引用的调用栈。以抓取进程号为 11237 的进程为例:
$ hiprofiler_cmd \ -c - \ -t 60 \ -o /data/local/tmp/hiprofiler_data.txt \ -s \ -k \<<CONFIGrequest_id: 1session_config { buffers { pages: 16384 }}plugin_configs { plugin_name: "nativehook" sample_interval: 5000 config_data { save_file: false smb_pages: 16384 max_stack_depth: 20 pid: 11237 string_compressed: true fp_unwind: true blocked: true callframe_compress: true record_accurately: true offline_symbolization: true startup_mode: false statistics_interval: 10 malloc_disable: true memtrace_enable: true restrace_tag: "RES_ARK_GLOBAL_HANDLE" js_stack_report: 1 max_js_stack_depth: 10 }}CONFIG说明:
• malloc_disable: true 与 memtrace_enable: true 配合 restrace_tag 使用,用于过滤常规 malloc 抓栈数据,仅采集指定的资源类型。
• js_stack_report: 1 开启跨语言回栈,回溯出 native 到 js 的调用栈,定位到 ArkTS 代码行。
• -o 指定的输出路径需以 /data/local/tmp 开头,否则可能采集不到数据。
2. 抓取指定进程 Local Handle 对象调用栈
从 API version 23 起支持 Local Handle 对象内存录制功能。Local Handle 对象内存录制功能要求被测应用在启动时自动替换并加载维测库后,才能正常采集 Local Handle 内存栈信息。以包名为 com.example.insight_test_stage 的进程为例,须在命令行中设置参数 startup_mode: true:
$ hiprofiler_cmd \ -c - \ -t 60 \ -o /data/local/tmp/hiprofiler_data.txt \ -s \ -k \<<CONFIGrequest_id: 1session_config { buffers { pages: 16384 }}plugin_configs { plugin_name: "nativehook" sample_interval: 5000 config_data { save_file: false smb_pages: 16384 max_stack_depth: 20 process_name: "com.example.insight_test_stage" string_compressed: true fp_unwind: true blocked: true callframe_compress: true record_accurately: true offline_symbolization: true startup_mode: true statistics_interval: 10 malloc_disable: true memtrace_enable: true restrace_tag: "RES_ARK_LOCAL_HANDLE" js_stack_report: 1 max_js_stack_depth: 10 }}CONFIG应用替换加载维测库方法:
• 应用处于退出状态:下发上述 Local Handle 录制命令(startup_mode 为 true),然后启动应用,应用启动后即可进行数据采集。
• 应用处于运行状态:下发录制命令(startup_mode 为 true),然后重启应用,应用重启后即可进行数据采集。
说明:
• 应用加载维测库后,只要应用不退出,维测库持续生效。此后可通过非启动模式录制 Local Handle 内存,此时 startup_mode 参数必须设置为 false。
• 使用此种方式后,此次应用打开的时长会变长,此次运行的性能上也会有损失,但不影响下次使用。
• 此种方式抓取到的 Local Handle 内存一定是泄漏的(未被回收的句柄才被记录)。
• 命令行方式获取的 trace 文件,可通过 DevEco Profiler 离线导入功能解析,单个文件大小不超过 1.5G。
• 从 API 版本 26.0.0 开始,统计模式支持采集 local/global handle 地址信息能力。
3. 文件导出
采集完成后,将设备上的 trace 文件导出到本地:
hdc file recv /data/local/tmp/hiprofiler_data.txt ./
4. DevEco Profiler 离线导入解析
首先将导出的.htrace 文件后缀改为.txt,然后在 DevEco Studio 的 Profiler 功能的会话区,点击 Open File 导入。该文件将会被自动解析为以下数据:
• Native Heap 子泳道(ArkLocalHandle/ArkGlobalHandle):展示分配统计信息,包括分配方式、总分配内存大小、总分配次数、尚未释放的内存大小与次数。
• Call Trees 标签页:展示内存分配栈,定位创建句柄的函数与所在 so 库。
• Allocations List 标签页:展示内存块起始地址、时间戳、活动状态、调用库与具体函数。
说明:Release 签名应用不支持跳转 Native 侧调用栈。开发者可双击可能存在问题的调用栈,跳转至相关代码执行分析、优化。
5. 结合 Node-API 代码定位与修复
定位到创建泄漏句柄的 Native 调用栈后,结合应用中的 Node-API 代码确认泄漏成因。以下是两类典型泄露场景代码示例:
(1)Global Handle 泄露场景
Global Handle 全局引用忘记 delete:
#include <napi.h>// 全局引用(泄漏重灾区)static napi_ref g_my_ref = nullptr;napi_value LeakRef(napi_env env, napi_callback_info info){ napi_value obj; napi_get_cb_info(env, info, nullptr, nullptr, &obj, nullptr); // 创建强引用(初始计数=1) napi_create_reference(env, obj, 1, &g_my_ref); // ❌ 只创建不释放 return nullptr;}// 缺少清理函数:// void Cleanup(napi_env env) { // if (g_my_ref) { // napi_delete_reference(env, g_my_ref); // g_my_ref = nullptr; // }// }Global Handle 循环/重复创建不释放:
napi_value CreateAndLeak(napi_env env, napi_callback_info info) { napi_value obj; napi_get_cb_info(env, info, nullptr, nullptr, &obj, nullptr); napi_ref ref; // 每次调用都新建引用 napi_create_reference(env, obj, 1, &ref); // ❌ 无delete // 错误:覆盖旧ref,旧ref句柄永久丢失 // g_ref = ref; return nullptr;}Global Handle 类/实例持有引用、析构不清理:
class NativeHolder { public: napi_ref m_ref; NativeHolder(napi_env env, napi_value obj) { napi_create_reference(env, obj, 1, &m_ref); } // ❌ 析构不delete ~NativeHolder() { // 缺少napi_delete_reference(env, m_ref)配对调用 }};napi_value CreateHolder(napi_env env, napi_callback_info info) { napi_value obj; napi_get_cb_info(env, info, nullptr, nullptr, &obj, nullptr); NativeHolder* holder = new NativeHolder(env, obj); // 若不主动清理:holder泄漏 + m_ref泄漏 return nullptr;}(2)Local Handle 泄露场景
Local Handle 局部引用忘记 close:
// 通过napi_open_handle_scope/napi_close_handle_scope管理本地句柄static napi_value HandleScopeTest(napi_env env, napi_callback_info info){ // 创建句柄作用域 napi_handle_scope scope; napi_open_handle_scope(env, &scope); // 在作用域内创建对象 napi_value obj = nullptr; napi_create_object(env, &obj); napi_value value = nullptr; napi_create_string_utf8(env, "handleScope", NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, obj, "key", value); // ❌忘记关闭句柄作用域 // napi_close_handle_scope(env, scope); return nullptr;}关于 Node-API 引用与作用域接口的完整使用规范(napi_open_escapable_handle_scope、napi_escape_handle、napi_reference_ref/unref、napi_get_reference_value、napi_add_finalizer 等),可参考 。
案例关联
应用侧亦可通过 Performance Analysis Kit 的 HiDebug 资源采集接口(OH_HiDebug_StartProfiler/OH_HiDebug_StopProfiler,资源类型 OH_RES_TYPE_GLOBAL_HANDLE,API 24.0 起)主动启动 Global Handle 分配栈采集,实现线上自诊断;
常见问题
现象 1:抓取到的 trace 文件为空。
可能原因与解决方法:检查 -o 指定的输出路径是否在 /data/local/tmp/ 目录下;若目标路径是该目录下的子文件夹,尝试对文件夹执行 chmod 777 操作;确认应用是否为 debug 签名应用。
现象 2:Service not started。
可能原因与解决方法:调优服务未能开启,说明正在使用 DevEco Studio 调优或上次调优异常退出,需执行 hiprofiler_cmd -k 之后再重新执行调优命令。
现象 3:Local Handle 采集无数据。
可能原因与解决方法:Local Handle 要求被测应用在启动时替换加载维测库。确认是否设置了 startup_mode: true,并按照"应用替换加载维测库方法"在命令下发后启动或重启应用。
现象 4:调优时目标进程卡顿。
可能原因与解决方法:适当减小 max_stack_depth 和 max_js_stack_depth 的值以减少回栈深度;适当增大 smb_pages 的值(默认 16384 页即 64M,可调整到 128M);适当增加 sample_interval 的值(默认 256,可调整到 512)。
现象 5:FP 回栈异常。
可能原因与解决方法:检查对应共享库(SO)编译时是否开启了 -fomit-frame-pointer 编译选项,若开启该选项则需要对其关闭(即启用-fno-omit-frame-pointer、-funwind-tables),否则 FP 回栈失效。若修改上述编译配置仍无法回栈,请改用 dwarf 回栈(fp_unwind 设为 false)。
示例代码
🔗 官网开发者学堂视频: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 技术交流群】





