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 |
|
对应Node-API类型 |
napi_value |
napi_ref |
|
生命周期管理方式 |
handle scope(作用域自动释放) |
手动创建/删除引用计数 |
|
泄漏典型原因 |
作用域未正确关闭、异步持有句柄 |
强引用创建后未delete |
|
采集标签 |
RES_ARK_LOCAL_HANDLE |
RES_ARK_GLOBAL_HANDLE |
|
IDE泳道呈现 |
Native Heap子泳道-ArkLocalHandle |
Native Heap子泳道-ArkGlobalHandle |
采集机制
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类型通过索引区分:
|
restrace类型 |
起始版本 |
|
RES_ARK_GLOBAL_HANDLE |
API 23 |
|
RES_ARK_LOCAL_HANDLE |
API 23 |
栈采集数据以protobuf格式写入htrace文件。API 26.0.0起,统计模式新增local/global handle地址和buildId信息。
生成数据说明
|
数据内容 |
说明 |
|
trace文件(.htrace) |
记录句柄创建的函数调用栈、线程与动态库维度的内存分配情况、调用栈次数与分配大小聚类信息。 |
|
调用栈 |
通过fp或dwarf回栈得到native栈,开启 js_stack_report 可从native向js层回栈,完成跨语言栈缝合,定位到创建句柄的ArkTS代码行。 |
|
地址信息 |
非统计模式实时返回;统计模式从API 26.0.0起支持采集Local Handle/Global Handle地址。 |
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%以上的对象。聚类规则主要有三种:
|
聚类规则 |
适用对象类型 |
|
最短引用链聚类 |
Method、js_set、js_map、string、JSNativePointer、jsarray等。对比各对象到GC ROOT的最短引用链(多条时取retained size最大者),相同则归为一类。建议基本类型对象默认采用此规则。 |
|
名字+引用链聚类 |
Function、framework、(array)、业务对象(业务侧创建的类对象和函数对象)。对象带路径名与行号,相同类/函数调用点不同则引用链不同,需按调用点区分。 |
|
属性+引用链聚类 |
jsobject、js_shared_object。对象名相同但语义不同,通过引用链区分调用点;distance为1的对象无法用引用链区分时,按直接持有对象的名称及持有关系归类。 |
特殊对象无需或另行处理: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环境已就绪。
规格说明
|
规格项 |
说明 |
|
起始版本 |
API 23 起支持 RES_ARK_LOCAL_HANDLE 与 RES_ARK_GLOBAL_HANDLE |
|
设备约束 |
Local Handle 仅支持 Phone 和 PC 设备 |
|
应用签名 |
仅支持使用调试证书签名的应用(debug签名应用) |
|
弱引用 |
采集时不抓取创建弱引用(引用计数为0的napi_ref)的调用栈 |
|
Local Handle启动要求 |
需在应用启动时替换加载维测库(startup_mode为true) |
|
统计模式地址 |
从 API 26.0.0 开始,统计模式支持采集 Local Handle/Global Handle 地址信息 |
|
性能影响 |
Local Handle替换维测库后本次运行打开时长变长、有性能损失,但不影响下次使用;建议仅在开发调试与压测阶段使用 |
|
输出路径 |
命令行 -o 指定的输出路径须以 /data/local/tmp 开头,子文件夹具备写权限 |
说明:若应用在生命周期内被强制终止后重启,再次录制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 \
<<CONFIG
request_id: 1
session_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 \
<<CONFIG
request_id: 1
session_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)。
示例代码
----------------------------------------------------------------------------------------------------
🔗 官网开发者学堂视频:华为开发者学堂

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


【扫码加入 HarmonyOS DFX 技术交流群】
更多推荐




所有评论(0)