APP闪退背锅侠找到了!NativeHeap 泄漏排查全攻略

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:
APP闪退背锅侠找到了!NativeHeap 泄漏排查全攻略
从“内存去哪儿了”到“揪出真凶”,一文搞定━
前言
你有没有遇到过这样的场景:App 用着用着就闪退了,日志里赫然写着”内存超限”——可你明明没有写什么大内存操作,内存到底去哪儿了?就像家里的水表每天都在转,但你找不到漏水点一样,NativeHeap 内存泄漏就是那个”看不见的漏水龙头”。
NativeHeap 是 C/C++ 原生代码通过 malloc、new 等方式动态申请的物理内存。当这些内存申请了却忘记释放,或者缓存策略设计不合理导致越积越多时,App 的 RSS 内存就会像吹气球一样不断膨胀,最终触发系统管控、导致闪退。
本文将以一个真实的 malloc 未释放案例为主线,带你从运维态和开发态两个维度,一步步揪出 NativeHeap 内存泄漏的”真凶”,并给出实用的修复建议,让你的App内存管理从此告别“盲盒模式”。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
一、什么是 NativeHeap 泄漏?——先搞清”漏水点”在哪
NativeHeap 堆内存,听起来很高大上,其实说白了就是 C/C++ 代码通过系统内存分配器(比如 malloc/calloc/realloc/free,或者 new/delete)动态申请的常驻物理内存。你可以把它想象成一个”内存仓库”——每次 malloc 就是从仓库里搬出一块内存,free 就是把不用的内存搬回去。
那什么是 NativeHeap 堆内存泄漏? 简单说,就是”搬出来的内存没搬回去”。应用因为申请的内存未释放、过量申请缓存等原因,导致 NativeHeap 堆越来越大,最终引发了 RSS 内存泄漏,轻则App卡顿,重则直接闪退。
为什么会发生 NativeHeap 泄漏?常见的“嫌疑人“有以下几位:
• 基础对象泄漏:手动 malloc/new 分配的内存,忘记手动 free/delete 释放。就像借了书不还,图书馆迟早倒闭。这是最经典也最容易避免的泄漏类型——每处malloc/new都必须要有对应的free/delete
• 循环引用:多个智能指针之间相互持有,构成环状引用,导致引用计数永远无法归零。好比两个人互相说“等你先走我再走“,结果谁也不走,内存自然就被占着了。
• 生命周期管理不当:通过系统接口申请了系统资源后,没有及时释放。资源就像租来的房子,到期不搬走就要被强制清退。
• 跨语言导致泄漏:ArkTS 对象持有 Native 对象,导致 Native 内存无法被回收。跨语言调用就像两个国家的人做生意,沟通不畅就容易出问题。
• 过量缓存:为了性能提升而设计缓存机制,但阈值不合理或代码流程出错,导致缓存超量使用,越积越多。就像为了省时间囤货,结果仓库被塞爆了。
• 业务过载:业务本身需要大量内存,相关资源对内存消耗极大,导致内存超限。这就像高峰期的高铁站,人太多挤爆了——不是管理有问题,而是需求确实超出了承载的能力。
二、运维态排查:从日志里”破案”
2.1 从 smaps 维测信息入手
当 App 发生 RSS 内存泄漏时,第一步就是看日志。开发者可以根据基础日志分析方法,对日志中的轻量化 smaps 维测信息进行分析。如果类型为”native heap”的内存最大,那么恭喜你——你已经把 RSS 内存泄漏问题精确到了 NativeHeap 内存泄漏。
怎么从日志里找线索?
分析轻量化 NMD 维测日志,排查可疑的内存泄漏区间。比如在某次实际排查中,我们发现以下异常数据:
# NMD 维测日志示例:展示不同 size 区间的内存分配情况
# size: 单次申请大小(字节),allocated: 当前已分配总量
# nmalloc: 总申请次数, ndalloc: 总释放次数
LOGGER_MEMCHECK_SAMPLE_NMD_INFO:
size allocated nmalloc ndalloc
8 17384 511848 509675
16 129376 338438 330352
32 1138816 1026155 990567
48 133161808 1322095 1256224 # 异常:32-48字节区间占用约133MB
64 351869376 908151 878942 # 异常:48-64字节区间占用约351MB
......
注意看:单次申请 32-48 字节范围内的堆内存有 133MB 左右(133161808 字节),48-64 字节范围内的堆内存有 351MB 左右(351869376 字节)。这两个区间的 allocated 值明显偏高,说明有大量小内存块未被释放——这就是可疑的泄漏区间。
2.2 通过内存调用栈精确定位
找到可疑的 size 区间后,下一步就是根据内存栈日志分析方法,找到 NativeHeap 内存调用栈。计算每份内存的 Bytes/Count,筛选符合可疑 size 范围的内存调用栈,通过调用栈进一步确认泄漏位置。

图1:内存调用栈分析界面,框中展示了每个调用栈的总内存Bytes与申请次数count
小贴士:应用内存栈受到应用内存泄漏的速度影响较大。对于快速泄漏问题,推荐参考订阅资源泄漏事件方法,在订阅资源泄漏事件的同时,通过 configEventPolicy() 方法补充订阅应用页面切换信息,以此分析泄漏场景,并在开发态对泄漏问题进行复现和定位。
三、开发态排查:用 Profiler “抓现行”
如果应用发生了 RSS 内存泄漏,开发者大致能推断出问题场景,那么可以通过复现此场景,使用 DevEco Studio 中 Profiler 工具的 Allocation 功能,直接”抓现行”——定位 NativeHeap 内存泄漏的具体代码位置。
简单来说,就是在代码里“埋下监控摄像头”,当内存异常增长时,工具会自动帮你抓到那个“偷内存”的代码段。
四、案例分析:malloc 申请内存未释放
本案例通过模拟动态通过 malloc 申请 NativeHeap 内存未释放,构造 NativeHeap 内存泄漏,最终触发系统管控,导致应用闪退。下面我们从运维态和开发态两个角度,完整走一遍排查流程。
4.1 运维态分析思路
步骤 1:获取故障日志
通过订阅资源泄漏事件,开发者可以在沙箱中接收到故障日志,路径为:
# 故障日志路径(XXXX为动态生成的时间戳)
/data/storage/el2/log/resourcelimit/RESOURCE_OVERLIMIT_XXXX_XXXX.log
步骤 2:分析 smaps 维测信息
在故障日志中找到关键字 LOGGER_MEMCHECK_SAMPLIFY_SMAPS_INFO,读取数据如下:
# smaps 维测日志:展示各类内存的 Size、Rss、Swap 等信息
# Category 列标识内存类型,如 arkts-static heap、AnonPage other、native heap 等
LOGGER_MEMCHECK_SAMPLIFY_SMAPS_INFO:
Size Rss Swap Category Counts Name
512 0 52 arkts-static heap 2 [anon:ArkTs Static Object Space]
256 0 224 arkts-static heap 1 [anon:ArkTs Static Non Movable Space]
8869848 4218636 2827804 AnonPage other 48 [anon]
1778944 1080 1052 ark ts heap 29 [anon:ArkTS Heap]
512 12 4 ark ts heap 2 [anon:ArkTS Heapread only space]
3072 2376 192 ark ts heap 12 [anon:ArkTS Heapnon movable space]
256 24 24 ark ts heap 1 [anon:ArkTS Heapshared non movable space]
256 28 20 ark ts heap 1 [anon:ArkTS Heapshared read only space]
40960 8 40472 ark ts heap 1 [anon:ArkTS Heapshared huge object space]
5888 5144 584 ark ts heap 23 [anon:ArkTS Heapappspawn space]
1792 460 1204 ark ts heap 7 [anon:ArkTS Heapsemi space]
768 108 540 ark ts heap 3 [anon:ArkTS Heapshared old space]
2560 228 1364 ark ts heap 10 [anon:ArkTS Heapshared appspawn space]
33554432 0 120 AnonPage other 5 [anon:partition_alloc]
步骤 3:按 Category 聚类分析
按照 Category 列进行聚类,对每种类型的 Rss 列、Swap 列求和,得到每种内存类型的申请总量。经过排序后发现,native heap 类型的内存总量最大——这就是我们要找的”头号嫌疑人”。
# 按 Category 聚类后的结果(节选)
Size Rss Swap Category Counts Name
8869848 4218636 2827804 AnonPage other 48 [anon]
步骤 4:分析 NMD 维测日志
获取轻量化 NMD 维测以及内存调用栈后,通过分析以下 NMD 维测日志,可以得出关键结论:402653184 字节的内存共申请了 4831838208 字节(约 4.5GB),导致应用发生了 RSS 内存泄漏。
# NMD 维测日志:展示各 size 区间的内存分配详情
# 重点关注最后一行:size=402653184 的内存 allocated=4831838208,ndalloc=0(从未释放)
LOGGER_MEMCHECK_SAMPLE_NMD_INFO:
size allocated nmalloc ndalloc
8 380464 335069 287511
16 595840 234006 196766
32 4292576 288151 154008
48 7271040 310762 159282
64 7685056 567100 447021
80 4250640 106810 53677
96 1257792 32049 18947
112 1301552 14495 2874
128 2095232 27505 11136
160 770080 9013 4200
......
393216 393216 3 2
458752 458752 1 0
524288 1048576 3 1
655360 655360 6 5
917504 917504 2 1
1048576 2097152 3 1
1835008 1835008 2 1
8388608 8388608 2 1
402653184 4831838208 12 0 # 异常:单次约384MB,申请12次共约4.5GB,从未释放
关键发现:size 为 402653184(约 384MB)的内存块,nmalloc=12(申请了 12 次),ndalloc=0(从未释放过一次)!allocated 总量高达 4831838208 字节(约 4.5GB)。这就是导致 RSS 内存泄漏的”罪魁祸首”。
步骤 5:使用 DevEco Studio Profiler 导入内存栈日志
1. 打开 DevEco Studio 中的 Profiler 组件,单击下图①处导入获取的内存栈日志。
2. 单击选择 All Heap 下的 Native Heap 泳道,如下图②处。
3. 单击③处 Call Trees 查看内存申请调用栈。
4. 单击④处选择 Created & Existing,筛选申请并且未释放的内存及其调用栈。
5. 找到内存申请异常的内存及其调用栈,如下图⑤处框选的内容。

图2:Profiler 中定位到异常内存调用栈,共申请6次,总计2.25GB,单次约402653184字节
从框选内容可以看出,这份内存栈共申请了 6 次,总共申请了 2.25GB 内存,单次申请内存约 402653184 字节,恰好与分析 NMD 维测日志得到的结果一致,进一步证实此调用栈为泄漏的内存栈。
步骤 6:定位泄漏代码
分析内存栈指向的代码段,可以得出应用正在循环申请一次超大内存,且未进行释放,最终导致了 RSS 内存泄漏。以下是泄漏代码的截图:

图3:泄漏代码截图——循环中调用 malloc 申请大内存,但未调用 free 释放
4.2 开发态分析思路
对于开发态存在的问题,开发者大致能够推断出当前出现 RSS 内存泄漏的场景,那么可以通过尝试复现此场景,并使用 DevEco Studio 中 Profiler 工具的 Allocation 功能,抓取内存异常增长的点。
步骤 1:复现场景并录制
在 DevEco Studio 中启动 Profiler 的 Allocation 录制功能,复现触发内存泄漏的操作流程。
步骤 2:查看 Native Heap 泳道
录制完成后,单击 All Heap 中的 Native Heap 泳道,发现 NativeHeap 内存异常增长了 4.5GB。

图4:开发态 Profiler 显示 NativeHeap 内存异常增长 4.5GB
步骤 3:通过 Call Trees 定位
点击①处 Call Trees 按钮,单击②处筛选 Created & Existing,可以找到异常申请的内存块和它的内存申请调用栈。

图5:Call Trees 中筛选出 Created & Existing 的异常内存调用栈
步骤 4:确认泄漏代码
分析内存栈指向的代码段,可以得出应用正在循环申请一次超大内存,且未进行释放,最终导致了 RSS 内存泄漏。

图6:泄漏代码——循环中持续 malloc 大内存,缺少对应的 free 调用
五、修复建议:给内存”上锁”
找到泄漏点后,怎么修?以下是两条核心修复原则,帮你从根本上杜绝 NativeHeap 内存泄漏。
5.1 分配与释放的成对匹配
核心原则:每处 malloc/new 必须有对应的 free/delete。
// 错误示例:申请了内存但没有释放
void badExample() {
char* buf = (char*)malloc(1024); // 申请了 1024 字节
// ... 使用 buf ...
// 忘记 free(buf) —— 内存泄漏!
}
// 正确示例:申请与释放成对出现
void goodExample() {
char* buf = (char*)malloc(1024); // 申请了 1024 字节
if (buf == NULL) return; // 防御性检查
// ... 使用 buf ...
free(buf); // 使用完毕后释放
buf = NULL; // 释放后置空,防止悬空指针
}
另外注意:重置指针前先释放。 如果要对指针重新赋值,确保先 free 掉旧内存,避免”旧内存无人认领”。
5.2 异常安全与提前返回路径
核心原则:用 RAII(Resource Acquisition Is Initialization)处理异常分支。
// 推荐:使用 RAII 模式管理资源生命周期
// C++ 中可以使用智能指针或自定义 RAII 包装器
class ScopedBuffer {
public:
ScopedBuffer(size_t size) : buf((char*)malloc(size)), size(size) {}
~ScopedBuffer() { free(buf); } // 析构时自动释放
char* get() { return buf; }
private:
char* buf;
size_t size;
};
// 使用 RAII 后,即使函数提前 return,内存也会被自动释放
void safeExample() {
ScopedBuffer buf(1024);
if (someCondition) return; // 提前返回,buf 的析构函数仍会执行
// ... 使用 buf.get() ...
}
同时注意:减少函数内多个 return 路径的重复释放代码。如果一个函数有多个 return 出口,每个出口都要确保资源已释放——或者更优雅地,使用 RAII 让编译器帮你管理。
六、总结与 Checklist
NativeHeap 内存泄漏就像App里的”慢性毒药”——平时不痛不痒,但日积月累下来,足以让一个好好的应用直接闪退。通过本文介绍的运维态和开发态排查方法,你可以系统地定位泄漏点:
• 运维态:通过 smaps 维测信息确认是否为 native heap 类型泄漏 → 分析 NMD 日志找到可疑 size 区间 → 通过内存调用栈精确定位
• 开发态:复现泄漏场景 → 使用 Profiler Allocation 功能录制 → 查看 Native Heap 泳道 → 通过 Call Trees 筛选 Created & Existing → 定位泄漏代码
• 修复:确保 malloc/free 成对匹配 → 使用 RAII 管理资源生命周期 → 减少多 return 路径的重复释放代码
最后提醒:对于快速泄漏问题,推荐在订阅资源泄漏事件的同时,通过 configEventPolicy() 方法补充订阅应用页面切换信息,以此分析泄漏场景,并在开发态对泄漏问题进行复现和定位。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
如果你在实际开发中遇到过类似的内存泄漏问题,欢迎在评论区分享你的排查经验!
希望这篇文章的经验能帮到正在与内存泄漏斗智斗勇的你。
🔗 官网开发者学堂视频: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协议
该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务
- 目前还没评论,等你发挥!

起点课堂会员权益



