资源泄漏别躲了,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特性指导
----------------------------------------------------------------------------------------------------------
🔗 官网开发者学堂视频:华为开发者学堂

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


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




所有评论(0)