【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 个,也同样值得关注。

未完待续......

参考资料

相关推荐
愤怒的代码3 小时前
🔗 深度解析 SystemUI 进程间通信机制(一)
android·操作系统·app
Xの哲學3 小时前
Linux Miscdevice深度剖析:从原理到实战的完整指南
linux·服务器·算法·架构·边缘计算
RainyJiang3 小时前
聊聊协程里的 Semaphore:别让协程挤爆门口
android·kotlin
Dev7z5 小时前
在MySQL里创建数据库
android·数据库·mysql
a努力。5 小时前
腾讯Java面试被问:String、StringBuffer、StringBuilder区别
java·开发语言·后端·面试·职场和发展·架构
invicinble5 小时前
mysql建立存数据的表(一)
android·数据库·mysql
麻辣兔变形记6 小时前
深入理解微服务下的 Saga 模式——以电商下单为例
微服务·云原生·架构
似霰6 小时前
传统 Hal 开发笔记1----传统 HAL简介
android·hal
Zender Han7 小时前
Flutter Gradients 全面指南:原理、类型与实战使用
android·flutter·ios