Local Handle与Global Handle内存分析实践
本原创文章帖发布在华为开发者联盟社区欢迎开发者前往访问评论交流更多与该内容相关讨论请点击原帖查看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. 性能应用占用内存持续增长系统为释放内存频繁触发GCGC执行时会暂停应用主线程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 HandleGlobal Handle对应Node-API类型napi_valuenapi_ref生命周期管理方式handle scope作用域自动释放手动创建/删除引用计数泄漏典型原因作用域未正确关闭、异步持有句柄强引用创建后未delete采集标签RES_ARK_LOCAL_HANDLERES_ARK_GLOBAL_HANDLEIDE泳道呈现Native Heap子泳道-ArkLocalHandleNative Heap子泳道-ArkGlobalHandle采集机制Local Handle与Global Handle采集能力由HiProfiler的native hook插件提供通过 restrace_tag 参数指定要采集的资源类型。支持两种采集入口• DevEco Studio ProfilerAllocation任务在All Heap Anonymous VM泳道的录制配置中通过 Record Data Range OptionsDevEco 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_HANDLEAPI 23RES_ARK_LOCAL_HANDLEAPI 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代码确认泄漏成因。以下是两类典型泄露场景代码示例1Global 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; }2Local 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_HANDLEAPI 24.0起主动启动 Global Handle 分配栈采集实现线上自诊断常见问题现象1抓取到的trace文件为空。可能原因与解决方法检查 -o 指定的输出路径是否在 /data/local/tmp/ 目录下若目标路径是该目录下的子文件夹尝试对文件夹执行 chmod 777 操作确认应用是否为debug签名应用。现象2Service not started。可能原因与解决方法调优服务未能开启说明正在使用DevEco Studio调优或上次调优异常退出需执行 hiprofiler_cmd -k 之后再重新执行调优命令。现象3Local Handle采集无数据。可能原因与解决方法Local Handle要求被测应用在启动时替换加载维测库。确认是否设置了 startup_mode: true并按照应用替换加载维测库方法在命令下发后启动或重启应用。现象4调优时目标进程卡顿。可能原因与解决方法适当减小 max_stack_depth 和 max_js_stack_depth 的值以减少回栈深度适当增大 smb_pages 的值默认16384页即64M可调整到128M适当增加 sample_interval 的值默认256可调整到512。现象5FP回栈异常。可能原因与解决方法检查对应共享库SO编译时是否开启了 -fomit-frame-pointer 编译选项若开启该选项则需要对其关闭即启用-fno-omit-frame-pointer、-funwind-tables否则FP回栈失效。若修改上述编译配置仍无法回栈请改用dwarf回栈fp_unwind设为false。示例代码• Node-API生命周期开发示例• HiProfiler性能分析工具----------------------------------------------------------------------------------------------------官网开发者学堂视频华为开发者学堂社区DFX专题文章华为开发者问答 | 华为开发者联盟【扫码加入 HarmonyOS DFX 技术交流群】