资源泄漏别躲了,HiDebug 来抓现行
应用一边跑,一边偷偷涨资源?
FD、线程、Native 内存、GPU 内存、Global Handle,到底是谁在“悄悄变胖”?
这一次,不靠玄学猜测,也不只盯着日志发呆。HarmonyOS 6.1 带来的 HiDebug 资源采集 API,可以让调用栈自己站出来说话。

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:
资源泄漏别躲了,HiDebug 来抓现行-华为开发者话题 | 华为开发者联盟
应用一边跑,一边偷偷涨资源?
FD、线程、Native 内存、GPU 内存、Global Handle,到底是谁在“悄悄变胖”?
这一次,不靠玄学猜测,也不只盯着日志发呆。HarmonyOS 6.1 带来的 HiDebug 资源采集 API,可以让调用栈自己站出来说话。
适合场景:HarmonyOS 应用稳定性建设、线上自诊断、DFX 调优、资源泄漏排查。
先说痛点:线上资源问题,真的很会藏 🙈
资源调优和资源泄漏,是鸿蒙应用开发中常见又棘手的两类问题。
很多时候,开发者能看到“现象”:
• 应用越跑越卡。
• 内存曲线一路抬头。
• 线程数不知不觉变多。
• FD 数量悄悄逼近上限。
• GPU 内存或 VM 堆使用率看起来不太对劲。
但要回答“是谁分配的”“从哪条调用链来的”“为什么没释放”,光靠流水日志、/proc/[pid]/smaps、/proc/meminfo、/proc/memview、hidumper –mem 往往还差一点火候。
HiDebug 资源采集 API 就是来补这一刀的。
通过 Performance Analysis Kit 开放的 OH_HiDebug_StartProfiler / OH_HiDebug_StopProfiler,应用可以在合适条件下采集资源分配栈,把“感觉有泄漏”变成“这里有证据”。
它能盯住哪些资源?👀
| 资源类型 | 说明 |
| 文件描述符 | 通过 open/fopen、epoll、eventfd、socket、pipe、dup 等创建的 FD。 |
| 线程 | 通过线程函数创建的线程,例如 pthread_create。 |
| Native 内存 | 通过 malloc/calloc/realloc/new 分配的堆内存,以及通过 mmap 映射的文件映射、匿名映射等内存。 |
| GPU 内存 | 通过 OpenGLES、Vulkan、OpenCL 等图形标准 API 创建的纹理、缓冲区等内存。 |
| 全局句柄 | Native 侧通过 napi_create_reference 创建的 ArkTS 对象全局引用,即 napi_ref。 |
简单说:FD 泄漏、线程泄漏、Native 内存涨、GPU 内存涨、Global Handle 忘记释放,都可以纳入观察范围。
核心接口:启动、停止,一套闭环 ⚙️
HarmonyOS 6.1 中,资源采集接口 API 24.0 的核心原型如下。
一个负责选择资源类型并启动采集,一个负责结束采集,结果通过回调返回。
// 资源类型
typedef enum OH_HiDebug_ResourceType {
OH_RES_TYPE_FD,
OH_RES_TYPE_THREAD,
OH_RES_TYPE_NATIVE,
OH_RES_TYPE_GPU,
OH_RES_TYPE_GLOBAL_HANDLE
} OH_HiDebug_ResourceType;
// 采集参数
typedef struct OH_HiDebug_ResProfilerConfig {
uint32_t maxDuration;
uint32_t filterSize;
uint32_t maxStackDepth;
uint32_t statisticsInterval;
uint32_t sampleInterval;
} OH_HiDebug_ResProfilerConfig;
// 采集结果
typedef struct OH_HiDebug_ProfilingResult {
OH_HiDebug_ResourceType resourceType;
const char* filePath;
} OH_HiDebug_ProfilingResult;
// 采集结果回调函数
typedef void (*OH_HiDebug_ProfilingCallback)(OH_HiDebug_ProfilingResult* result);
// 启动采集
HiDebug_ErrorCode OH_HiDebug_StartProfiler(
OH_HiDebug_ResourceType type,
OH_HiDebug_ResProfilerConfig* config,
OH_HiDebug_ProfilingCallback callback);
// 停止采集
HiDebug_ErrorCode OH_HiDebug_StopProfiler(void);
type:先决定要抓谁 🎯
type 参数用于选择采集资源类型,支持以下五类:
• OH_RES_TYPE_FD:文件描述符。
• OH_RES_TYPE_THREAD:线程。
• OH_RES_TYPE_NATIVE:Native 内存。
• OH_RES_TYPE_GPU:GPU 内存。
• OH_RES_TYPE_GLOBAL_HANDLE:全局句柄。
你怀疑谁不老实,就先把观察镜头对准谁。
config:让采集既有用,也别太打扰用户 🧭
config 采集参数支持五种配置项。它们的核心价值,是在“采得够准”和“别把应用拖慢”之间做平衡。
| 参数 | 描述 | 建议 |
| maxDuration | 最大采集时间,单位为秒,最大支持 1 小时。 | 根据实际开销调整。 |
| filterSize | 过滤小于等于 filter_size 的内存分配栈,减少采集数据量。 | 数据太多时可适当增大。 |
| maxStackDepth | 设置最大资源分配调用栈回栈深度。 | 建议默认值 30。 |
| statisticsInterval | 统计间隔,将一个统计周期内的栈进行汇总,单位为秒。 | 建议默认值 10s。 |
| sampleInterval | 采样大小,单位为字节,采样率为 1/采样大小。 | 建议默认值 384 bytes;Native 内存分配小于 384 字节按 1/384 采样,大于等于 384 字节全采。 |
小提醒 💡
采集参数可根据实际采集开销情况调整,目标是达成性能损耗与应用体验之间的自适应平衡。默认值只是参考,不是放之四海皆准的魔法数字。
callback:采完以后去哪儿看 📦
采集完成后,结果会通过 callback 返回。
| 回调参数 | 描述 |
| resourceType | 采集资源类型。 |
| filePath | 资源分配栈 .htrace 文件的沙箱路径。 |
开发者可以拿到 .htrace 文件路径后,将文件上传到云侧做泄漏根因分析、聚类,或者导入 IDE 查看 Call Trees。
这就很关键了:资源问题终于可以从“猜一猜”进入“查一查”阶段。
实现逻辑:本质上就是把分配路径照亮 💡
资源分配栈采集的核心逻辑,是对不同资源的分配和释放函数进行跟踪,然后记录调用栈、去重、符号化,并最终以 protobuf 格式的 .htrace 文件落盘至应用沙箱。
不同资源的采集逻辑大致如下:
• FD:hook 文件描述符打开和关闭函数,并记录调用栈。
• 线程:hook 线程创建和销毁函数,并记录调用栈。
• NativeHeap:hook musl 库 malloc/calloc/realloc/free、mmap/munmap 函数,并记录调用栈。
• GPU 内存:hook OpenGLES、Vulkan、OpenCL 的 texture、buffer 创建和释放函数,并记录调用栈。
• Global Handle:hook global handle 对象的创建和释放函数,并记录调用栈。
换句话说,它不是只告诉你“涨了”,而是尽力告诉你“从哪儿涨起来的”。
什么时候触发?别手滑,要有策略 🚦
线上采集最讲究“该出手时再出手”。
下面是推荐触发条件,可作为接入策略参考:
| 资源类型 | 推荐触发条件 | 检测间隔 | 参考建议 |
| 文件描述符 FD | 超过 5000 个 | 60s | 可读取 /proc/self/fd_num 获取 FD 总数。1 个检测周期内超过阈值,可认为可能存在 FD 泄漏。 |
| 线程 | 超过 700 个 | 60s | 可读取 /proc/self/status 的 Threads 字段。线程总数超过 700 个时,可触发采栈。 |
| Native 内存 | 超过 3 GB | 200s | 可读取 /proc/self/status,将 VmRss + VmSwap 作为 Native 内存总量。连续 2 个检测周期均超过 3GB,可认为可能存在泄漏。 |
| GPU 内存 | 超过 2300 MB | 60s | 可调用 OH_HiDebug_GetGraphicsMemory 获取 GPU 内存总量。以 12G 手机为例,连续 2 个周期均超过 2.3GB 可触发。 |
| Global Handle | VM 堆内存使用率超过 70% | 60s | 可用 hidebug.getAppVMObjectUsedSize() / hidebug.getAppVMMemoryInfo().totalHeap > 0.7 作为触发条件。 |
注意 ⚠️
应用 Native 内存超过 4GB,且整机内存压力过大时可能触发系统管控。实践中建议超过 3GB 或更小时就启动采集,别等问题冲到脸上才开始找伞。
规格约束:强能力,也有边界感 🧱
HiDebug 资源采集能力很实用,但它不是无限量供应的“随便采套餐”。
采集配额限制
• 整机所有应用共享:每日最多可采集 4 次;同一时刻最高支持 4 个不同应用并行采集。
• 单应用独享:每日最多可采集 2 次;同一时刻最高支持应用内 2 个进程并行采集。
系统负载熔断机制
当系统触发以下任一负载保护条件时,API 采集请求可能被拒绝,并返回相应错误码:
• 整机系统 CPU 占用率超过 70%。
• 整机剩余可用内存 RAM 或剩余可用存储空间 ROM 低于影响应用体验和整机读写性能的阈值。
负载保护条件可能随系统版本迭代优化,具体以实际返回错误码为准。
并发冲突约束
本 API 与命令行工具或系统采集任务存在排他性。
当与命令行工具或系统采集任务发生冲突时,API 采集请求将被拒绝并返回相应错误码。
采集结果交付
• 存储路径:采集到的调用栈日志文件自动保存在应用沙箱目录 /data/storage/el2/base/files/。
• 文件名规则:资源采集类型-进程名-进程号-时间戳.htrace。
老化管控
每种资源类型均受以下条件约束:
• 按文件个数管控:只保存最新 5 份文件,滚动老化。
• 按文件大小管控:单文件上限 1G,达到上限即停止,避免文件过大导致磁盘压力或 IDE 无法加载。
• 按目录空间管控:沙箱下采集文件存储空间超过 4G 时,删除最早的采集文件。
• 按存储时效管控:单个日志最大存储 24 小时,到期后清理。
系统采集模式优先级
• 命令行模式 hdc shell hiprofiler_cmd 优先,可抢占 API 采集、系统采集、应用灰度采集。
• API 采集与系统采集、应用灰度采集并发时,按 FIFO 原则处理,先发起的模式优先响应,其它模式拒绝访问。
再敲一下重点 🛎️
本 API 不保证每次采集请求都成功。调用它会对当前应用进程及整机性能功耗产生不可忽略的影响,包括但不限于 CPU、内存占用升高、丢帧卡顿、发热。线上环境严禁盲目或高频次触发。
推荐接入姿势:稳一点,更香 ✅
想把 HiDebug 资源采集能力接进线上诊断链路,建议遵循这几个原则:
1. 只在灰度范围内开启,避免影响全部用户。
2. 只在资源超过业务基线或推荐阈值时触发。
3. 采集前检查系统负载,避免在高压状态下继续加压。
4. 合理配置 maxDuration、maxStackDepth、statisticsInterval、sampleInterval。
5. 采集完成后及时上传、分析、清理,避免日志文件堆积。
6. 将 .htrace 文件与业务场景、版本、设备信息关联,方便后续聚类分析。
这才是线上诊断的正确打开方式:精准出手,拿到证据,快速闭环。
Global Handle 泄漏:几个高频现场 🧯
Global Handle 泄漏是 Native 侧很容易踩的坑。下面三个例子很典型,适合作为排查时的“对照组”。
场景一:全局引用忘记 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;
// }
// }
这个场景很常见:创建引用时很顺手,释放时忘得很自然。
但全局引用不会自己消失,时间一长就会变成稳定性隐患。
场景二:循环或重复创建不释放
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;
// 每次调用都新建引用,但没有 delete
napi_create_reference(env, obj, 1, &ref);
// 如果覆盖旧 ref,旧句柄也会永久丢失
// g_ref = ref;
return nullptr;
}
一次泄漏看起来不显眼,循环调用就很热闹。
这类问题最适合用资源分配栈采集来确认调用来源。
场景三:类或实例持有引用,析构不清理
class NativeHolder {
public:
napi_ref m_ref;
NativeHolder(napi_env env, napi_value obj)
{
napi_create_reference(env, obj, 1, &m_ref);
}
// 析构时缺少 napi_delete_reference(env, m_ref)
~NativeHolder()
{
}
};
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;
}
类实例和引用绑定在一起时,生命周期管理尤其要小心。
对象销毁、引用释放、异常路径回收,都要配套考虑。
一句话总结 ✨
如果你的 HarmonyOS 应用正在做稳定性建设、性能优化或线上自诊断,HiDebug 资源采集 API 值得加入工具箱。
它不能替你自动修 Bug,但能把“谁分配、谁没放、从哪条调用链来”照得更清楚。
资源泄漏再会藏,也怕调用栈开灯。把 HiDebug 接入诊断链路,让线上问题少一点玄学,多一点证据。
参考链接 🔗
• 官方文档:OH_HiDebug_StartProfiler
• 原文:资源采集API特性指导
———————————————————————————————————-
🔗 官网开发者学堂视频: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 技术交流群】
本文由 @华为开发者联盟 授权发布于人人都是产品经理。未经作者许可,禁止转载
题图来自Unsplash,基于CC0协议
该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务
- 目前还没评论,等你发挥!

起点课堂会员权益



