【Perfetto从入门到精通】2. 使用 Perfetto 追踪/分析 APP 的 Native/Java 内存

这个世界就是这样,你从失败中学到的东西可能比成功中学到的东西更多------《Android 传奇》

说起 Android APP 内存分析,我们第一时间想到的,往往是 Android Studio Profiler、MAT 这样的老牌工具,而 Perfetto 的出现,又为其提供了一种更加贴近底层的视角。而且相比于现有的工具,Perfetto 更加擅长于分析 Native 内存占用,可以说是补齐了工程师在这方面的短板。

在内存方向,我计划用2~3篇文章来介绍 Perfetto 的功能、特点、使用方法等 。作为第1篇,本文包含以下内容:

  • Android APP 所申请的内存分类(Java、Native)
  • 如何抓取、分析这两类内存占用情况,并提供对应的 Perfetto 操作实例

APP 占用内存分类

Android 系统框架是由 Linux 发展而来的,对于每一个应用程序(APP),框架为其初始分配一个进程,并启动了一个 ART 虚拟机(更早时候是 Dalvik),这个虚拟机用于解释执行 Java 字节码。因此,一个 APP 所占用的内存,可以归结为两类:在 Linux 层申请的 Native 内存,以及在 JVM 上申请的 Java 内存。

  • Native 内存 :由 C/C++/Rust 进程申请,其底层使用 libcmalloc()/free() 函数进行内存申请/释放。此外,APP 通过 JNI 也可申请此部分内存,例如 java.util.regex.Pattern 类的大部分功能,就同时申请了 Native 内存和 Java 内存。
  • Java 内存 :由应用内的 Java/Kotlin 代码,通过 new X() 的方式申请,位于 上,由系统自动进行回收。

针对上述内存使用场景,Perfetto 提供了两种技术进行分析。

  • heap profiling(堆分析) 适用于 Native 代码,通过 AOP,追踪 malloc/free 函数调用链,从而识别出每个函数引起的内存变化,集成在火焰图当中展示。
  • heap dump(堆转储) 适用于 Java 代码,展示对象之间的引用关系,但不包含调用链。

Native (C/C++/Rust) 内存分析

在 C/C++/Rust 语言中,使用底层的 malloc/free 函数进行内存申请和释放,Perfetto 通过 AOP 技术,在这些函数内部注入代码进行拦截,因此能够识别每一次内存的细微变动。出于在应用性能和抓取效果之间保持平衡的考虑,还提供了 sampling(采样) 机制。

需要注意的是,Native 内存分析是 不可追溯 的,它只能记录从 启动 Perfetto 的时间点之后 的内存分配。这意味着,如果内存问题已经发生后,再进行 Native 内存追踪,会有无法识别引起内存问题的函数的风险(内存泄漏则不一样,因为它无时无刻不在发生)。

案例:NowInAndroid

我们知道,在 Android 7.1(API 25)之前,Bitmap 对象的像素数据是使用 Dalvik heap 存储的,在拿到 heap 文件后,可以直接导出并看到图片内容。从 Android 7.1 开始,Bitmap 的存储位置从 Dalvik heap 转移到了 Native heap。这样一来,虽然可以避免大量图片资源耗尽 JVM 内存,但也容易让人忽视掉 Native 内存管理,导致 OOM 发生。

Perfetto 的 Native 内存分析,可以用来识别这种落在 Native heap 的内存分配调用链。我们以 NowInAndroid 项目为例。

APP 的首页是图文元素构成的瀑布流,在频繁滑动的情况下,会产生大量 Native 内存申请/回收的现象,我们通过 Perfetto 对此进行观察。

在网页端开启 Memory 目录下的 Native heap profiling 开关,同时填入待分析的包名 com.google.samples.apps.nowinandroid.demo.debug,可以通过命令 adb shell ps -A 来查看进程对应的包名。

控制台参数如下:

plaintext 复制代码
buffers {
  size_kb: 65536
  fill_policy: DISCARD
}
data_sources {
  config {
    name: "android.heapprofd"
    heapprofd_config {
      sampling_interval_bytes: 4096
      process_cmdline: "com.google.samples.apps.nowinandroid.demo.debug"
      shmem_size_bytes: 8388608
      block_client: true
      all_heaps: false
    }
  }
}
duration_ms: 10000

接下来点击 Start tracing 开始抓取,在 10s 的抓取过程中,上滑页面,以加载更多图片和文字。抓取结果如下:

图例比较简单,分成左上角的"数据选择 "、右上角的"展示方式 "和占据正中央大部分的"火焰图",依次介绍之。

"数据选择"区域,共有4种排序方式,分别是:

  • Unreleased Malloc Size(未释放内存分配大小) :默认模式,按未释放内存 字节数 的总和聚合调用栈。
  • Unreleased Malloc Count(未释放内存分配计数) :按 计数聚合未释放的内存分配,忽略每次分配的大小。这有助于发现小规模的内存泄漏,即每个对象都很小,但随着时间的推移,大量对象会累积起来。通常称之为 内存碎片
  • Total Malloc Size(总内存分配大小) :按通过 malloc() 分配的内存字节数聚合调用栈,无论这些内存是否已被释放。这有助于调查堆频繁调用,即即使最终释放了内存,也会对分配器造成很大压力的代码路径。通常称之为 内存波动
  • Total Malloc Count(总内存分配计数) :与上述类似,但按 malloc() 调用次数聚合,忽略每次分配的大小。

我们目前选择的是默认的 Unreleased Malloc Size 选项。

"展示方式"区域,分为"自顶向下"和"自底向上",前者的火焰向下生长,后者反之,不赘述。

"火焰图"区域,是性能分析的重点,通过层叠的形式展示函数调用链路,每个色块(Block)的面积表示该函数引起的内存增加,发生在相同层级的同一个函数,会被聚合展示。图中的火焰图可以看出,NowInAndroid 代码写的相对健壮,回收和复用都做得比较好,因此 RecyclerView 中未见内存泄漏。

Java 内存快照

在 JVM 系语言中,通过自动回收机制管理内存,大部分 Profiling 工具都是进行内存快照,计算当前虚拟机的 全部对象 及其 引用关系,从而形成一张完整的对象图。

Java 内存快照的不足之处是,无法通过它查看函数调用链路,即哪些函数引起了多大的内存增长。

开始抓取后,上滑页面直至完成10s的抓取,以下是抓取到的结果。

可以看到,Bitmap 占据了内存的大部分。此外,String数量有 96433 个,也同样值得关注。

未完待续......

参考资料

相关推荐
HwJack2024 分钟前
【HarmonyOS开发小实践】ArkUI 动画系统属性动画、显式动画与路径动画
ui·性能优化·harmonyos
BoomHe2 小时前
Android Framework 文件应用移植到 AndroidStudio
android
一尘之中3 小时前
指挥控制中心与指控系统:概念、应用、厂商与DDS技术全景
学习·架构·ai写作
传奇开心果编程3 小时前
【Jetpack Compose基础语法学与练】第8课 rememberSaveable,页面旋转/系统重建保留状态
android·学习·ui·kotlin·android jetpack
徐小夕4 小时前
存量.doc 文档怎么办?JitWord 开源转换库打通旧文档到在线协同全链路
后端·架构·github
>Andre<4 小时前
UFS5.0标准中文全译·卷一:范围、术语与架构
android·linux·嵌入式硬件
安易算力4 小时前
PUE优化工程实践:从1.5到1.2的制冷架构与气流组织改造路径
网络·python·容器·架构·kubernetes
这个DBA有点耶4 小时前
InnoDB索引组织表下,复合主键和自增主键的物理存储差异与选型对比
数据库·mysql·架构
事圆则缓4 小时前
Java 8 Lambda、Stream、Optional 与 Android 边界:从回调语法到运行时兼容
android·java
mmsx5 小时前
Android 地图十万要素不卡顿:空间网格 + 渐进式加载的移动端实践
android·大数据·opengl