开发态快速定位ArkTS泄漏
头像 鸿蒙小助手 2026-06-29 11:26:46    发布
4109 浏览 15 点赞 4 收藏

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

开发态快速定位ArkTS泄漏-华为开发者话题 | 华为开发者联盟

1.1 ArkTS内存泄漏分析流程与高频泄漏场景

1.1.1 术语介绍


术语解释
GC RootGC Root(Garbage Collection Root,垃圾回收引用链根节点) 是垃圾回收器进行可达性分析的起点。从 GC Root 能访问到的节点对象是“存活的”,否则会被回收。
引用链在 ArkTS 中,“引用链”就是从某个GC Root(如VMRoot、FrameRoot等)出发,经过一连串的对象引用,最终到达目标对象的路径。如果这个路径存在,则该对象是 “存活” 的,不会被 GC 回收; 如果从任何 GC Root 都无法找到一条到达该对象的引用链,该对象就是 “不可达” 的,会被标记为垃圾对象并回收。
VMRootVMRoot是 ArkTS 虚拟机层面的根引用集合,代表 GC 遍历的起点。
FrameRootFrameRoot是函数调用栈帧在 GC 遍历过程中的根节点。当函数被调用时,其局部变量和入参对象会被当前栈帧引用,从而成为 GC 的“可达”起点。
Local HandleLocal Handle(本地句柄)是一种短期引用,用于在本地作用域(如函数调用栈)内持有对象,防止对象被垃圾回收。当作用域结束时,这些句柄会自动释放。
Global HandleGlobal Handle(全局句柄)例如 napi_ref,用于长期持有对象引用,确保在对象生命周期内不会被垃圾回收机制回收。使用此类句柄时,开发者需要手动管理其生命周期,包括创建和销毁,以避免潜在的内存泄漏问题。

1.1.2 高频泄漏场景

1.1.2.1 JS对象被VMRoot类型持有导致内存泄漏

      常见构成 VMRoot 引用的来源包括:

      • 模块导出对象:export 出的对象被底层的 SourceTextModule 系统对象持有,而模块本身在应用生命周期内不被卸载。

      • 全局对象属性:通过 globalThis.xxx = ... 挂载的对象,globalThis 是贯穿应用始终的根对象。

      • 内置原型链扩展:修改 Array.prototype、Object.prototype 等内置对象原型,导致意外全局持有。

      一旦业务对象挂载到上述根节点上,即使页面销毁、组件卸载,该对象依然被 VMRoot 强引用,GC 无法回收。

1.1.2.2 JS对象被Local Handle/Global Handle引用导致内存泄漏

      在鸿蒙的JS-Native交互中,JS对象可以通过NAPI Native代码访问或持有。Native代码为了引用这些JS对象,会使用两种主要的句柄类型:napi_value、napi_ref。其中,napi_ref 是一种引用计数的句柄,用于保持对JS对象的引用,防止其在Native代码持有引用期间被垃圾回收器回收。

      • Local Handle (napi_value): 通常指在Native代码执行上下文中创建的、作用域较短的引用。当Native       代码执行环境切换时,这些Local Handle通常会被自动清理。Local Handle受Handle Scope管理,大部分场景下(如同步调用、napi框架异步调用等)系统会为创建的Local Handle添加Handle Scope,但仍有部分场景(如libuv异步调用等)系统不会主动添加Handle Scope,需要应用自行添加Handle Scope,否则就会导致JS对象无法回收。

      • Global Handle (napi_ref): 这是一种作用域为整个应用生命周期的引用。一旦创建,除非显式删除,否则它会一直保持对JS对象的引用。通常用于需要跨模块、跨上下文甚至跨JS线程访问的JS对象。由于其持久性,如果不加注意,很容易成为JS对象泄漏的根源。

      当Native代码持有(无论是local还是global)一个JS对象时,它实际上建立了一种强引用关系。在JS引擎的垃圾回收机制中,如果一个JS对象的所有可达性引用(包括JS代码内部的引用、DOM树等)都被清除,该对象就可以被回收。然而,如果存在一个Native句柄指向它,那么这个句柄就构成了一个阻止根,使得该JS对象在Native代码持有该句柄期间,从垃圾回收器的角度看,它是“可达”的。这就阻止了JS对象被回收,从而可能导致内存泄漏。

1.1.2.3 JS对象被FrameRoot类型持有导致内存泄漏

      正常情况下,函数执行完毕退栈后,栈帧销毁,这些临时引用自动失效,内存随之释放。

然而,若函数长期不退栈,局部变量/参数所引用的对象将持续被 FrameRoot 锚定,即便业务逻辑已不再需要它们,GC 也无法回收。常见导致栈帧滞留的情形包括:

      • 死循环或无限递归,函数永不返回。

      • 在函数内启动了一个长期运行的同步阻塞操作(如同步网络请求、大文件同步读写)。

      • 函数内部创建了闭包并被外部长期持有,且闭包捕获了该函数栈帧中的变量(导致整个栈帧无法释放)。

      • 使用了生成器(Generator)或 async/await 但未正确消费,导致协程挂起,栈帧保留。

1.1.3 标准化排查流程

      复现与观察:使用DevEco Profiler的Realtime Monitor(Memory泳道),重复多次进出目标页面,观察内存曲线是否呈阶梯状上升。

      识别泄漏点:在操作前后各采集一次堆快照,使用 Snapshot 对比功能,关注新增对象数量和 Shallow Size,从而识别泄漏点。

      查看对象引用链:在Snapshot快照的对象引用链中找到异常存活对象(如本该销毁的Component实例),通过“Shortest Paths”分析引用链情况。

      1. 是否 VMRoot 持有:若引用链顶端为 VMRoot / SourceTextModule,多为模块导出单例或全局变量未清理。

      2. 是否 Local/Global Handle 持有:若泄漏对象的Distance为1,则多为Native侧持有未释放导致。可分为2种场景:

          a. Local Handle:napi_value未使用napi_open_handle_scope 或未成对使用(即使用napi_open_handle_scope但是未使用napi_close_handle_scope 释放)

          b. Global Handle:napi_ref 未调用 napi_delete_reference 释放或未成对使用(即使用napi_create_reference但是未使用napi_delete_reference 释放)在这2种情况下,无法基于引用链分析,需要通过Allocation模板,开启Local Handle及Global Handle录制选项来进一步分析。

      3. 是否 FrameRoot 持有:若非VMRoot和Local/Global Handle持有,则多为函数不退栈或闭包捕获对象被外部持有。

      代码审查:根据引用链中的关键节点(如 EventHub、Timer、全局变量、napi_ref、闭包上下文)定位代码中的持有关系。

      修复与验证:修改代码后重复步骤 1~2,确认内存曲线回归平稳。

图1

1.2 ArkTS内存泄漏分析案例

1.2.1 案例背景

      现象:本案例中,通过复现‘首页至消息页’的反复进出操作,观察到应用内存占用呈现"阶梯式持续增长"趋势。在循环操作10次后,应用出现显著卡顿现象。

      初步判断:典型的“阶梯式内存增长”,高度疑似内存泄漏。

1.2.2 分析流程

1.2.2.1 步骤1:Memory泳道确认泄漏

      1. 打开 DevEco Studio,连接真机,点击 Profiler工具Realtime Monitor(也可使用snapshot模板的Memory录制观察,以下是使用Realtime Monitor观察)。

      2. 启动应用,选择设备与应用进程。

      3. 点击录制按钮,在设备上重复操作:进入消息页面 → 停留2秒 → 返回首页,重复多次。

      4. 观察内存曲线:

          • 正常预期:每次退出后内存回落至基线附近,整体呈锯齿状。

          • 实际现象:曲线呈阶梯状上升,多次操作后内存增长至314.1MB且无明显回落。

      ✅ 确认存在内存泄漏,进入下一步。

图2

1.2.2.2 步骤2:堆快照对比定位异常对象

      1. 在 Profiler 中切换到 Snapshot 模板,请参考使用 Snapshot 模板基本操作 :选择Profiler工具 → 选择设备与应用进程 → 选择Snapshot模板 → 创建Session → 启动录制

图3

      2. 抓取快照1:首次在进入消息页前,点击“Take Heap Snapshot”。

图4

      3. 抓取快照2:重复进出消息页面7次后,回到首页,点击“Take Heap Snapshot”,再抓取第二次快照,并停止录制。

图5

      4. 在快照对比视图Comparison中,选择 CompareTo Snapshot1

图6

      5. 查看对象新增销毁情况,优先关注:

          • 操作次数的整数倍整数倍+1(export 出的对象本身也有一条引用链),

          • 业务对象,即Constructor的结构为包名/模块名/文件路径#泄漏对象

图7

      关键发现

          • 对比结果中,Test 对象实例数量从 0个 增加到 8个

          • 正常情况下,页面退出后组件实例应被回收,快照2中应只有1个Test 对象(被export持有)。

      ✅ 确认 Test 对象实例泄漏。

1.2.2.3 步骤3:追踪引用链

      1. 在快照对比视图中,展开并选中一个 Test 对象实例,优先选Distance数量较多的,此处我们选 “6” 的,并打开右侧详细信息面板。

      注:选择 Distance 数量较多的对象实例,主要是因为这些实例频繁地被创建且未能得到及时释放。这表明存在潜在的内存泄漏问题,需要进一步调查以确定具体的泄漏源。

图8

2. 点击 “Shortest Paths” 获得如下Test 对象实例的最短引用链:

图9

1.2.2.4 步骤4:分析VMRoot类型持有导致泄漏问题

      1. 根据步骤3得到的Test 对象实例的最短引用链分析,该引用链的GC Root为 SourceTextModule 符合被export模块导出对象持有,VMRoot泄漏类型场景。

图10

      2. 从GC Root向上排查引用链,找到第一个业务对象,即CacheTest.ts文件中的CacheTest对象的cache属性持有了Test 对象未释放。点击后面跳转图标,打开CacheTest对象实例详细面板

图11

      3. CacheTest对象实例面板中,点击对象名后跳转按钮,跳转对应代码行。

图12

      4. 跳转代码后分析,CacheTest对象存在静态属性cache,该静态属性中保存了Test 对象。

图13


     5. 结合业务代码分析,MessageCenterPage 组件创建时会保存一个Test 对象到CacheTest中,但组件销毁时未清理该缓存导致内存泄漏。

图14

      6. 修改代码,在页面销毁时清除缓存。修改后重新复现操作7次,并抓快照验证,Test 对象创建数量正常。但分析发现Proxy 对象数量异常,创建70个未销毁。

图15

      7. 参考 “步骤3:追踪引用链” ,找到最短引用链,发现该 Proxy 对象的 Distance为1。说明其为GC Root直接持有的对象,被Native侧直接持有未释放,即JS对象被Local Handle/Global Handle引用导致内存泄露

图16

      8. 通过 Proxy 对象的 Distance为1的References,确认内存泄露是由JS对象被Global Handle引用导致

图17

1.2.2.5 步骤5:分析Local Handle/Global Handle类型持有导致泄漏问题

      基于步骤4分析得到 Proxy 对象实例可能被Local Handle/Global Handle引用导致内存泄露,我们可以通过以下步骤继续分析定位:

      1. 配置Allocation录制模板并捕获数据

      • 打开DevEco Studio:确保你的工程已加载,并连接了目标设备或模拟器。

      • 进入Profiler模块:在主界面下方菜单栏,找到并点击Profiler选项卡。

      • 选择应用进程:运行应用,并在 “区域2” 选择目标设备和应用进程。

      • 创建Allocation录制模板:选择 “Allocation” 并点击 Create Session 创建录制模板

图18

      • 配置录制参数

           ▪ 配置模式:选择详情模式(即关闭Statistics Mode)。当前仅详情模式支持进行ArkTS和Native的关联分析

          ▪ 配置开关勾选“Local Handle”和“Global Handle”,这是关键配置。这将使Allocation专门捕获与JS-NAPI句柄相关的内存分配事件。

             ○ 如果底层镜像不支持该功能,则会提示“当前镜像版本不支持,请升级镜像”;

图19

          ▪ 配置泳道范围:勾选 ArkTS Snapshot泳道。这将使Allocation在录制结尾时自动抓取一份Snapshot快照用于关联分析。

图20

          ▪ 启动录制:勾选了“Local Handle”开关后,如果是在应用本生命周期内首次录制local handle数据,会触发弹窗请求重启应用以便录制对应信息,此时点击OK允许重启即可。

图21

          ▪ 运行应用程序:运行目标应用,执行相关被怀疑引入内存泄露的业务操作,持续一段时间以增加内存压力和捕获更多数据。

          ▪ 停止录制:自动触发抓取一份Snapshot快照用于关联分析。点击快照,查找到疑似泄漏对象 Proxy 。

图22

      2. 泄漏对象关联分析

          • 定位可疑ArkTS对象:选中一个怀疑被泄漏的ArkTS对象实例(或对象类型),查看扩展标签页。

图23

          • 查看Native List:若某个 ArkTS 对象的 distance 值为 1,则可以通过扩展标签页中的 Native List 标签页,查看所有当前与该 JS 对象关联的 Native 句柄引用,以确认该 JS 对象是被 Local HandleGlobal Handle 引用的对象。

图24

          • 关键信息

            1. 句柄类型:调用栈底层的符号ArkGlobalHandle或ArkLocalHandle判断泄漏类型

            2. 调用栈:通过调用栈,可以定位到应用的Native代码(可能是ArkUI框架代码或你自己代码)中创建napi_ref的地方。

            3. 注意点

               ▪ 如果该JS对象节点不是一个被Local Handle或者 Global Handle引用的对象,则会提示 "No Detail";

               ▪ 如果该JS对象确实是一个被Local Handle或者 Global Handle引用的对象,但是对应的native 内存的申请事件已经在此次录制之前完成内存分配,本次录制结果则无法展示对应的内存申请调用栈,需要重新录制,录制时需要注意将录制时执行的业务逻辑范围调整的尽量更早一些。

      3. 分析内存分配调用栈

          • 排查调用栈:“Native List”标签页中的调用栈,找到对应业务代码

          • 关键排查点

             ▪ 检查是否在适当的时候调用了对应的句柄释放接口如napi_delete_reference等。

             ▪ 梳理这段Native代码需要引用ArkTS对象的合理性,识别这个引用的生命周期是否过长,是否应该在某个条件满足后被释放;

             ▪ 对于napi_ref,其引用计数是关键。确保在不再需要引用时正确调用了napi_delete_reference。注意,引用计数可能因其他代码路径的创建或删除操作而意外增减。

             ▪ 检查句柄的作用域是否合理。local handle是否应该只在JS执行上下文切换前使用?global handle是否真的需要在整个应用生命周期都有效 。

图25

1.2.3 修复验证

      1. 重新运行应用,再次使用 Memory 泳道监控。

      2. 重复多次进出消息中心页。

      3. 验证结果

          • 内存曲线恢复锯齿状,每次退出后回落至基线。

          • 再次抓取快照对比,多次操作后业务对象实例数量无明显增长。

          • 泄漏问题已修复。

图26

图27

1.3 附录1 ArkTS泄漏根因

      ArkTS运行时采用分代回收模型 ,将对象按生命周期划分为新生代和老年代。使用标记-清除(mark-and-sweep)算法回收内存,所有可达对象被标记为存活,不可达对象则被回收。每次GC都是从根节点开始遍历,因此根节点被称为GC Root。该树的快照如图1所示。

图28

      理解GC的核心在于一个关键洞察:GC只回收“无法从GC Root到达”的对象。只要你的对象还挂在那棵引用树上,哪怕你已经忘记它、不再需要它,GC也无法回收。换句话说,内存泄漏的本质不是GC失效,而是开发者留下了不该留的引用链。因此排查内存泄漏的关键,就是找到那条从GC Root出发、让无用对象“存活”的引用路径,并在适当位置将其切断。


©本站发布的所有内容,包括但不限于文字、图片、音频、视频、图表、标志、标识、广告、商标、商号、域名、软件、程序等,除特别标明外,均来源于网络或用户投稿,版权归原作者或原出处所有。我们致力于保护原作者版权,若涉及版权问题,请及时联系我们进行处理。
分类
HarmonyOS
头像

鸿蒙小助手

致力于为鸿蒙开发者谋福利

1099

帖子

8

提问

13572

粉丝

关注
热门推荐
地址:北京市朝阳区北三环东路三元桥曙光西里甲1号第三置业A座1508室 商务内容合作QQ:2291221 电话:13391790444或(010)62178877
版权所有:电脑商情信息服务集团 北京赢邦策略咨询有限责任公司
声明:本媒体部分图片、文章来源于网络,版权归原作者所有,我司致力于保护作者版权,如有侵权,请与我司联系删除

京ICP备:2022009079号-2

京公网安备:11010502051901号

ICP证:京B2-20230255