HarmonyOS开发中,如何高效定位并分析内存泄露相关问题
- (1)Allocation的应用
- 调试方式
- Memory泳道
- Native Allocation泳道
- (2)Snapshot
- (3)ASan的应用
- 使用约束
- 配置参数
- 使能ASan
- 方式一
- 方式二
- 启用ASan
- ASan检测异常码
- (4)HWASan的应用
- 功能介绍
- 约束条件
- 使能HWASan
- 方式一
- 方式二
- 启用HWASan
- 总结
应用在开发过程中,可能会因为API使用错误、变量未及时释放、异常频繁创建/释放内存等情况引发各种内存问题。
华为官方提供了多种方案来方便各位开发者分析解决内存相关问题,比如Allocation (获取native调用栈profiler)、SnapShot、ASan、HWASan,本文重点对这些工具的特点做些说明,同时结合实际案例对这些工具的应用做进一步讲解。
(1)Allocation的应用
Allocation是DevEco Studio 开发工具的 Profiler提供的内存场景分析工具,开发过程中可以使用Allocation来分析应用或服务在运行时的内存分配及使用情况,识别和定位内存泄漏、内存抖动以及内存溢出等问题,对应用或服务的内存使用进行优化。
调试方式
打开 DevEco Studio 找到 Profiler,DevEco Profiler提供Launch、ArkUI、Frame、Concurrency、ArkWeb、Network、Time、Allocation、Snapshot、CPU等场景化分析任务类型,在设备列表中选择设备(目前只支持真机),在进程列表中选择要调测的应用,选择Allocation并点击Create Session即可创建一次会话。右侧任务分析窗口可以选择Memory、Native Allocation具体分析。
Memory泳道
可以看到Memory泳道包含三个标识:PSS代表进程独占内存和按比例分配共享库占用内存之和,RSS是进程独占内存和相关共享库占用内存之和,USS代表进程独占内存。
在右边录制详情区域,工具控制栏上有很多小图标,鼠标放上去会有一些功能提示,可以添加一些录制选项,各泳道区域也有下拉框选项,下拉选择不同的设置可以调整录制功能。
单击任务窗口左上角的 ,启动录制,也可以选择左侧的任务列表中的,启动录制后,等待任务状态由“initializing”变为“recording”。录制过程中整个DevEco Profiler不能再点击其他的模板进行操作,如果想录制其他模板可以结束本次录制重新选择其他模板开始录制。
录制过程中,右侧任务分析窗口显示未recording状态,先要结束此次录制时,点击左侧Allocation按钮的stop按钮即可,结束录制之后可以看到当前session的各个不同内存类型对应的变化情况。
默认展示其中的五个子泳道,如要显示其他子泳道,可以点击主泳道的options标签并勾选其他泳道来查看。
选择具体的子泳道可以在details模块看到不同的时间点,对应的内存值变化情况,方便开发者进一步定位问题。
特别提示:
- 由于隐私安全政策,已上架应用市场的应用不支持录制此泳道。
- 建议避免同时录制ArkTS Allocation及Native Allocation泳道,避免影响分析准确性。
Native Allocation泳道
Native Allocation泳道主要显示具体的Native内存分配情况,包括静态统计数据、分配栈、每层函数栈消耗的Native内存等信息。由于隐私安全政策,已上架应用市场的应用不支持录制此泳道。通过操作对应的options同样可以选择展示的类型,框选子泳道后显示具体的内存分配,包括静态统计数据、分配栈等。
如下图所示,为选中Native Allocation后的某种展示场景。
- Statistics显示该段时间内的静态分配情况。包括分配方式(Malloc或Mmap)、总分配内存大小、总分配次数、尚未释放的内存大小、尚未释放次数、已释放的内存大小、已释放次数。
点击任意对象上的跳转按钮,可跳转至此类对象的详细占用/分配信息。当前统计模式下不支持跳转。 - Call Trees页签显示线程的内存分配栈情况。包括函数地址或符号、分配大小、占比以及函数栈帧的类别等。单击任一行栈帧,“More”区域将显示经过该栈帧的分配内存最大的调用栈。
- Allocations List显示内存分配的详细信息。包括内存块起始地址、时间戳、当前活动状态、大小、调用的库、调用库的具体函数、事件类型(与Statistics页签的分配方式对应)等。
针对详情面板中所展示的函数栈帧信息(如下图所示),双击栈帧结点,工具便会在编辑器中打开相关源码文件,并定位到对应行号。此功能正常使用的前提是用于抓取性能数据的应用,是在DevEco Studio所在的开发环境中编译,且相关源文件位置并未改变。
值得注意的是,上图对应的category列表标识调用栈的类型,从语言层面分为ArkTS、NAPI以及Native,从归属层面分为开发者代码以及系统代码(如下图所示)。从这两个方面可以将调用栈类型归类如下:
- ArkTS:程序正在执行ArkTS代码;
- NAPI:程序正在运行的NAPI代码;
- Native:程序正在执行的Native代码;其中每一个类型的亮色和灰色分别代表开发者和系统的代码。
Native Allocation泳道的内存状态信息可以进一步过滤和筛选。选中Native Allocation后,details底部有两个默认过滤条件All Allocations、Native Size。
-
All Allocations(默认状态):详情区域展示当前框选时间段内的所有内存分配信息
-
Created & Existing:详情区域展示当前框选时间段内分配未释放的内存。
-
Created & Released:详情区域展示当前框选时间段内分配已释放的内存。
-
Native Size:详情区域按照对象的原生内存进行展示。
-
Native Library:详情区域按照对象的so库进行展示。
此外,在“Native Allocation”泳道的“Allocations List”页签中还可以通过so库名称进行筛选、搜索关键词筛选、内存分配堆栈进行筛选,此处不再做过多演示。
关于Allocation的更多介绍和使用方法可以在官方文档查看,基础内存分析:Allocation分析。
(2)Snapshot
Snapshot是DevEco Profiler提供的一个内存快照分析工具,通过结合Memory实时占用情况,分析不同时刻的方舟虚拟机内存对象占用情况及差异,进而进行内存优化。
关于Snapshot具体的应用技巧可以借鉴官网Snapshot模板基本操作,或者借鉴使用Snapshot Insight分析ArkTS内存问题,本文不再赘述。
(3)ASan的应用
ASan(Address-Sanitizer)是内存检测的工具,用于发现内存飞踩第一现场,DevEco Studio为开发者集成了ASan能力,可以检测C/C++的地址越界问题,解决一些踩内存导致的异常crash的补充手段,对于一些明显不可能crash的场景可以尝试开启ASan。
使用约束
- 如果应用内的任一模块使能ASan,那么entry模块需同时使能ASan。
- 如果entry模块未使能ASan,该应用在启动时将闪退,出现CPP Crash报错。
- ASan、TSan、HWASan不能同时开启,三个只能开启其中一个。
配置参数
ASAN_OPTIONS:在运行时配置ASan的行为,包括设置检测级别、输出格式、内存错误报告的详细程度等。ASAN_OPTIONS支持在app.json5中配置,也支持在Run/Debug Configurations中配置。app.json5的优先级较高,即两种方式都配置后,以app.json5中的配置为准。
以在app.json5中配置环境变量为例。打开AppScope > app.json5文件,添加配置示例如下。
{"app": {"appEnvironments": [{"name": "ASAN_OPTIONS","value": "log_exe_name=true abort_on_error=0 print_cmdline=true" // 示例仅供参考,具体以实际为准},],...}
}
在Run/Debug Configurations中配置环境变量,具体可查看配置环境变量。
使能ASan
可通过以下两种方式使能ASan。
方式一
- 在运行调试窗口(entry --> Edit Configurations),点击Diagnostics,勾选Address Sanitizer。
- 如果有引用本地library,需在library模块的build-profile.json5文件中,配置arguments字段值为“-DOHOS_ENABLE_ASAN=ON”,表示以ASan模式编译so文件。
方式二
- 修改工程目录下AppScope/app.json5,添加ASan配置开关。
"asanEnabled": true
- 设置模块级构建ASan插桩。
在需要使能ASan的模块中,通过添加构建参数开启ASan检测插桩,在对应模块的模块级build-profile.json5中添加命令参数:
"arguments": "-DOHOS_ENABLE_ASAN=ON"
启用ASan
运行或调试当前应用。
当程序出现内存错误时,弹出ASan log信息,点击信息中的链接即可跳转至引起内存错误的代码处。
举例说明:
编写一段数组越界的C++代码,对外暴露接口,在ArkTs侧调用该方法。
Text(this.message).fontSize(50).fontWeight(FontWeight.Bold).onClick(() => {hilog.info(0x0000, 'testTag', 'Test NAPI 2 + 3 = %{public}d', testNapi.add(2, 3));hilog.info(0x0000, 'testTag', 'HeapBufferOverflow() ', testNapi.HeapBufferOverflow());// this.memoryLeak()})
运行当前项目,触发Button的onClick事件调用之后,应用崩溃,弹出FaultLog信息。
点击 Jump to Log跳转到FaultLog模块。
可以看到log信息有明确的log信息,报错原因为“AddressSanitizer: heap-buffer-overflow”,同样的还有多个链接地址。“appspawn30149==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x005b7fe7153b at pc 0x007c24fc309c bp 0x007eba84b160 sp 0x007eba84b158
”可以直接跳转到官网关于“heap-buffer-overflow”的报错解释以及解决方案。另外多个包含“/data/storage/el1/…”的地址能精确定位到C/C++层代码出错的具体位置。
然后我们就发现这是数组越界造成的,所以解决方案就是“对于已知大小的集合,注意访问不要越界,位置大小的集合访问前先判断大小。”
ASan检测异常码
ASan不仅能检测出“heap-buffer-overflow”异常,还能检测很多其他类型的内存异常,关于这些异常问题及解决方案,如下所示。
-
heap-buffer-overflow
- 背景/原理:访问越界。
- 影响/报错:导致程序存在安全漏洞,并有崩溃风险。
开启ASan检测后,触发demo中的函数,应用闪退报ASan,包含字段:AddressSanitizer:heap-buffer-overflow - 修改方法:注意数组容量不要访问越界。
- 推荐建议:已知大小的集合注意访问不要越界,位置大小的集合访问前先判断大小。
-
stack-buffer-underflow
- 背景/原理:访问越下界。
- 影响/报错:导致程序存在安全漏洞,并有崩溃风险。
开启ASan检测后,触发demo中的函数,应用闪退报ASan,包含字段:AddressSanitizer:stack-buffer-underflow - 修改方法:访问索引不应小于下界。
- 推荐建议:访问索引不应小于下界。
-
stack-use-after-scope
- 背景/原理:栈变量在作用域之外被使用。
- 影响/报错:导致程序存在安全漏洞,并有崩溃风险。
开启ASan检测后,触发demo中的函数,应用闪退报ASan,包含字段:AddressSanitizer:stack-use-after-scope - 修改方法:在作用域内使用该变量。
- 推荐建议:注意变量作用域。
-
attempt-free-nonallocated-memory
- 背景/原理:尝试释放了非堆对象(non-heap object)或未分配内存。
- 影响/报错:导致程序存在安全漏洞,并有崩溃风险。
开启ASan检测后,触发demo中的函数,应用闪退报ASan,包含字段:
AddressSanitizer: attempting free on address which was not malloc()-ed - 修改方法:不要对非堆对象或未分配的内存使用free函数。
- 推荐建议:不要对非堆对象或未分配的内存使用free函数。
-
double-free
- 背景/原理:重复释放内存。
- 影响/报错:导致程序存在安全漏洞,并有崩溃风险。
开启ASan检测后,触发demo中的函数,应用闪退报ASan,包含字段:
AddressSanitizer: double-free - 修改方法:已经释放一次的指针,不要再重复释放。
- 推荐建议:变量定义声明时初始化为NULL,释放内存后也应立即将变量重置为NULL,这样每次释放之前都可以通过判断变量是否为NULL来判断是否可以释放。
-
heap-use-after-free
- 背景/原理:当指针指向的内存被释放后,仍然通过该指针访问已经被释放的内存,就会触发heap-use-after-free。
- 影响/报错:导致程序存在安全漏洞,并有崩溃风险。
开启ASan检测后,触发demo中的函数,应用闪退报ASan,包含字段:
AddressSanitizer: heap-use-after-free - 修改方法:已经释放的指针不要再使用,将指针设置为NULL/nullptr。
- 推荐建议:实现一个free()的替代版本或者 delete析构器来保证指针的重置。
-
Other categories
未知的错误类型,持续更新中。
(4)HWASan的应用
HWASan(Hardware-Assisted Address Sanitizer)是一款类似于ASan的内存错误检测工具,目前仅适用于。 与ASan相比,HWASan使用的内存减少很多,因而更适合用于整个系统的清理。
功能介绍
与ASan相比,HWASan具有如下特征:
- CPU开销约为2倍。
- 代码大小开销为40% - 50%。
- RAM开销为10% - 35%。
HWASan能检测到ASan所能检测到的同一系列错误:
- 堆栈和堆缓冲区上溢/下溢。
- 释放之后的堆使用情况。
- 重复释放/错误释放。
和ASan相比,HWASan具有以下优点:
- HWASan不需要安全区来检测buffer overflow,既极大地降低了工具对于内存的消耗,也不会出现ASan中某些overflow检测不到的情况。
- HWASan不需要隔离区来检测UseAfterFree,因此不会出现ASan中某些UseAfterFree检测不到的情况。
- 此外,HWASan还可以检测返回之后的堆栈使用情况。
约束条件
- HWASan检测仅适用于AArch64架构的硬件。
- ASan、TSan、HWASan不能同时开启,三个只能开启其中一个。
使能HWASan
方式一
在运行调试窗口(例如entry --> Edit Configurations),点击Diagnostics,勾选Hardware-Assisted Address Sanitizer开启检测。
方式二
修改工程目录下的AppScope/app.json5文件,添加HWASan配置开关。
"hwasanEnabled": true
在需要使能HWASan的模块中,通过添加构建参数开启HWASan检测插桩,在对应模块的模块级build-profile.json5中添加命令参数:
"arguments": "-DOHOS_ENABLE_HWASAN=ON"
启用HWASan
运行或调试当前应用。当程序出现内存错误时,弹出HWASan log信息,点击信息中的链接即可跳转至引起内存错误的代码处。
关于HWAan的代码实例和应用场景基本和ASan一致,此处就不做重复是说明了。
总结
-
根据Allocation分析的结果,可以方便开发者优化应用的服务和组件的内存使用。例如,减少不必要的内存分配和释放,优化数据结构的使用等。
-
通过ASan或HWASan分析问题时,精准定位内存出错原因和具体位置,开发者应修复这些内存管理的问题,如确保所有分配的内存都被正确释放。
-
开发过程中,使用合适的内存管理和回收机制 :确保应用在不需要内存资源时能够有效地回收和管理这些资源,避免过度使用内存。