本文是 Android Studio 4.1 中 Profiler 有哪些新增特性的第二部分。之前的文章侧重于介绍 Android Studio 中 System Trace 的新增功能。
我们从大家的反馈了解到使用 C++ 调试本地内存非常困难,尤其在开发游戏的时候。在 Android Studio 4.1 中,Memory Profiler (内存分析器) 可以记录本地内存分配的调用栈。本地内存记录基于 Perfetto 后端实现,它是 Android 的新一代性能工具和问题追溯解决方案。
在调试内存问题的时候,通常的做法是搞清楚什么在占用内存,什么在释放内存。本文接下来会带着大家一起使用 Native Memory Profiler 来发现内存泄漏,并且使用 GPU 模拟压力测试 (Gpu Emulation Stress Test) 作为示例工程。
Perfetto
https://perfetto.dev/
GPU 模拟压力测试 (Gpu Emulation Stress Test)
https://github.com/google/gpu-emulation-stress-test
准备工作
首先,我们从 https://github.com/google/gpu-emulation-stress-test 克隆或者下载源码。
当我们发现可疑的内存泄漏时,最好的做法是从更高的层次开始并且观察系统内存的图形。您只需要在 Android Studio 中点击 profile 按钮,然后打开内存分析器,里面会显示更加详细的内存追踪信息。
内存分析器的顶层视图,从显示中可以看到每次运行 "GPU emulation stress test" 的时候内存占用都会逐步升高
运行了几次模拟器后,我们可以发现一些有趣的现象:
对于 GPU 模拟应用来说,GPU 内存增加是理所应当的,然而 Activity 被 finish 之后,该内存似乎被清空了。
每当我们打开 GPUEmulationStressTestActivity 的时候,本地内存都会有所增加,但是每次运行后该内存似乎并没有被重置,这就表明可能存在内存泄漏。
Native Memory Table (本地内存表格) 视图
从 Android Studio 4.1 Canary 6 开始,我们可以通过获取本地内存分配记录来分析为何内存未被释放。为了能够在 GPU 模拟应用上进行该项操作,我先停止正在运行的应用,然后启动分析一个新的实例。从一个初始的状态开始,会有助于我们缩小需要关注的范围,尤其是在研究一套不熟悉的代码的时候更是如此。通过内存分析器,我可以获得整个 GPU 模拟示例运行期间的本地内存分配记录。我们需要点击 Run->Profile-> ‘app’ 来重启应用。应用启动后 profile 窗口会打开,点击内存分析器,然后选择 "record native allocation"。
本地内存记录在 Android Studio 中加载时的初始状态
有些游戏或者应用所依赖的库会在 new 关键字之外调用 malloc 来申请内存。这个表格视图突出显示了这种情况,因而在应对这类游戏或应用时非常有用。
当记录加载后,数据会以表格的形式呈现。表格中会显示调用 malloc 的叶子函数。除了显示函数名,表格里还会包含模块、调用计数、空间大小、和 delta 值。这些信息会被进行采样,因此不是所有的 malloc 或 free 的调用都会被捕捉到。这很大程度上取决于采样率,后面我们会讨论它。
另外很有必要了解这些占用内存的函数是被哪些函数调用的。有两种方法可图形化该信息。第一种方法是将 "Arrange by allocation method" 选项改为 "Arrange by call stack"。表格会显示调用栈的树结构,和 CPU 记录里的类似。如果当前项目包含符号 (通常适用于可调试构建,如果您正在分析一个外部的 APK,可以参考一下文档),他们会自动被选取并启用。这样您就可以通过右键点击函数并 "Jump to source" 来直接转向源码。
文档
https://developer.android.google.cn/studio/debug/apk-debugger#attach_native
在表格里右键点击一个元素会显示 "Jump to Source" 菜单
内存可视化 (本地和非本地)
我们还在内存分析器中增加了用于可视化数据的火焰图,您可以非常快速地找到分配内存最多的调用栈。该方法对于很深的调用栈非常有用。
有四种方式可以在 X 轴上对数据进行排序:
"分配容量" (Allocation Size) 属于默认值,表示被追踪的内存总量;
"分配计数" (Allocation Count) 表示分配内存的对象总数;
"全部剩余容量" (Total Remaining Size) 表示在数据采集结束之前,整个数据采集过程中未被释放的内存容量;
"全部剩余计数" (Total Remaining Count) 和剩余容量类似,表示在采集结束之前,整个采集过程中未被释放的对象总数。
采集数据加载之后,在 "全部剩余容量" 视图里,很容易发现 "lodepng" 所分配的内存容量比较大
从这里我们可以直接右键点击调用栈,然后选择 "转向源码" (Jump to Source),然后会直接转向内存分配相关的源码。不过,我们稍微花些时间看一下这里的可视化图形,会发现这里共享的父节点 WorldState 造成了多个泄漏问题。要验证这点,可以通过图形来过滤结果。
过滤/导航
和表格视图类似,图表可以通过过滤栏 (filter bar) 进行数据过滤。当启用过滤的时候,图表的数据会自动进行更新,仅显示函数符合关键词或者正则表达式的调用栈。
有的时候调用栈会比较长,或者仅仅因为屏幕的空间不足而无法完整显示全部函数的名称。您可以使用 ctrl 加鼠标滚轮进行缩放,或者可以点击图表,使用 W、A、S、D 进行导航。
验证结果
增加断点,并且快速运行两次模拟器,然后发现第二次运行的时候,由于我们覆盖了第一次运行时的一个指针造成了内存泄漏。
调试器的 Quick 视图显示第二次运行的时候 "sWorld" 已经有值了
作为快速解决方案,我们可以在处理结束后释放掉 sWorld 变量,然后再次分析应用来验证问题是否解决。
我们还是观察高层次的内存统计。验证了在模拟运行结束的时候删除 sWorld 释放了最初运行时占用的 70 MB。
应用启动分析和采样率设置
上面的例子展示了如何通过本地内存追踪来定位和解决内存泄漏问题。另一个本地内存追踪的常见用法是理解应用启动时内存的占用情况。在 Android Studio 4.1 中,我们还增加了在应用启动时采集本地内存使用记录的功能。您可以在 "Run/Debug Configuration" 里的 "Profiling" 标签页进行设置。
Profiling 标签页位于 Run Configuration 对话框中
您可以在 Run 配置对话框中自定义采集间隔或者设置应用启动时记录内存使用情况。
这里您还可以为新的采集修改采样率。更小的采样率会对整个性能产生很大的影响,而更大的采样率则会遗漏一些内存分配记录。不同的采样率针对不同类型的内存问题。
总结一下
通过全新的本地内存分析器可以定位内存泄漏并且轻松洞悉内存使用情况。快去 Android Studio 4.1 试试本地内存分析器吧。如果有任何问题和反馈可以给我们留言。更多小窍门可以查阅我们今年早些时候在 Google 游戏峰会分享的内容:
给我们留言
https://issuetracker.google.com/issues/new?component=192708&template=840533
腾讯视频链接
https://v.qq.com/x/page/c0947m7dzuy.html
Bilibili 视频链接
https://www.bilibili.com/video/BV18k4y1R7Fc
推荐阅读
点击屏末 | 阅读原文 | 使用 Memory Profiler 查看 Java 堆和内存分配