写点什么

Local Handle 与 Global Handle 内存分析实践

  • 2026-08-03
    北京
  • 本文字数:6783 字

    阅读完需:约 22 分钟

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看: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)。

示例代码

• Node-API生命周期开发示例

• HiProfiler性能分析工具


🔗 官网开发者学堂视频: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 技术交流群】