探讨Android内存管理

前言

在看这篇文章之前,需要Linux内存管理基础,推荐先学习一些Linux内存管理基础知识点,这里我们在做一些应用层的优化工作,但从底层理解一些原理性的知识点,对我们有较大帮助。

主要讨论四点内容:

  • Android 内存管理机制
  • 如何评估 App 的内存占用
  • 一些减少 App 内存占用的建议

内存管理概述

Android 运行时 (ART) 和 Dalvik 虚拟机使用分页内存映射来管理内存。这意味着应用修改的任何内存,无论修改的方式是分配新对象还是轻触内存映射的页面,都会一直驻留在 RAM 中,并且无法换出。要从应用中释放内存,只能释放应用保留的对象引用,使内存可供垃圾回收器回收。这种情况有一个例外:对于任何未经修改的内存映射文件(如代码),如果系统想要在其他位置使用其内存,可将其从 RAM 中换出。

  • 分页

页是一种内存管理技术,它允许进程的物理内存不连续。它通过在称为页面(Page)的相同大小的块中分配内存来消除碎片问题,是目前比较优秀的内存管理技术。分页将物理内存划分为多个大小相等的块,称为帧(Frame)。并将进程的逻辑内存空间也划分为大小相等的块,称为页面(Page),通过页表(Page Table)用于查找此刻存储特定页面的帧,Android 使用的分页稍有不一样的地方

  • 内存映射(binder、thread、内存分配、io)

内存映射(mmap)是一种内存映射文件的方法,即将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和应用程序进程虚拟地址空间中一段虚拟地址的一一映射关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上。应用程序处理映射部分如同访问主存。

了解Android内存使用情况

Android内存管理方式,通常,Android设备的内存分为很多页(Page),每页大约4 KB。

Image source: Google I/O 18

图片

  1. Used Pages: 这些是进程当前正在使用的页面.
  2. Cached Pages: 进程正在使用的页面,但是内存的某些部分也存在于主内存中。因此,为了快速检索数据,我们使用缓存的页面,是可以被回收的。
  3. **Free Pages:**空闲页

下面看看内存压力对设备的影响。显示了一段时间内的内存使用情况,当 Cached Pages 低于 LMK 阈值时,将会触发低内存杀死机制。

图片

所以,你可以想象 LMK 在低内存手机上的情景:

图片

Linux Kernel 将会持续跟踪每个进程使用的页(Pages),所以只要对进程使用的 Pages 进行统计

图片

有些 Pages 是进程间共享的:

图片

那么问题来了?如何处理此共享内存。是由应用程序负责共享内存,还是由Google Play服务负责此共享内存?有几种不同的方法可用于处理这些情况:

  1. RSS (Resident Set Size): 应用程序负责所有共享内存,使用Google Play服务的应用将负责内存共享。
  2. USS (Unique Shared Set): 应用程序不负责任何共享页面。
  3. **PSS (Proportional Set Size)😗*应用将负责与共享共享内存的进程数成比例的页面数统计。

但是总的来说,很难找到应用程序是否需要共享内存。因此,我们使用“比例集大小”方法。PSS避免了过多计算或低估了共享页面对设备的总体影响。

通过上面我们知道,Android使用 分页 内存映射 来构建虚拟内存,同时使用** 垃圾回收器** 来 回收内存 ,值得注意的是, 应用修改的任何内存 ,无论修改的方式是分配新对象还是轻触内存映射的页面, 都会一直驻留在 RAM 中 ,并且不会换出到磁盘。使用 LowMemoryKiller (LMK)在低内存的时候来 杀死进程 释放更多内存,使用PSS来评估应用占用的内存,

如何计算内存?

图片

  • Java:从 Java 或 Kotlin 代码分配的对象的内存。
  • Native:从 C 或 C++ 代码分配的对象的内存。
    即使您的应用中不使用 C++,您也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存代表您处理各种任务,如处理图像资源和其他图形时,即使您编写的代码采用 Java 或 Kotlin 语言。
  • Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
  • Stack:您的应用中的原生堆栈和 Java 堆栈使用的内存。这通常与您的应用运行多少线程有关。
  • Code:您的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。
  • Others:您的应用使用的系统不确定如何分类的内存。
  • Allocated:您的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象。

下面是运行时内存占用介绍:

图片

Java/Native/Heap

您应用中的 Dalvik 分配所占用的 RAM。Pss Total 包括所有 Zygote 分配(如上述 PSS 定义中所述,通过进程之间共享的内存容量来衡量)。Private Dirty 值是仅分配给您的应用堆的实际 RAM,包含了您自己的分配和任何 Zygote 分配页,这些分配页在从 Zygote 派生您的应用进程以来已被修改。

Code

.so mmap 和 .dex mmap:

映射的 .so(原生)和 .dex(Dalvik 或 ART)代码占用的 RAM。Pss Total 值包括应用之间共享的平台代码;Private Clean 是您的应用自己的代码。通常,实际映射的内存容量要大得多。此处的 RAM 只是应用已执行的代码当前需要占用的 RAM。不过,.so mmap 具有较大容量的私有脏 RAM,这是因为在将其加载到最终地址时对原生代码进行了修复。

.oat mmap:

这是代码映像占用的 RAM 容量,根据由多个应用共用的预加载类计算。此映像在所有应用之间共享,不受特定应用影响。

.art mmap:

这是堆映像占用的 RAM 容量,根据由多个应用共用的预加载类计算。此映像在所有应用之间共享,不受特定应用影响。尽管 ART 映像包含 Object 实例,但它不会计入您的堆占用空间。

以上Code占用内存**,我们可以通过“adb shell cat /proce/PID/smaps”直接将这个虚拟文件的信息打印在控制台上,它实际上是应用的用户空间**地址的内存分配表,记录了应用分配的每一块内存的地址,类别,大小等信息。

但是应用所使用的全部内存里面,有一些内存块是不映射到进程的用户空间地址空间的(主要是GPU所使用的内存),这些内存块的信息在smaps里面无法找到,所以在Android 4.4里面新增了一个memtrack的HAL模块由SoC厂商实现,如果SoC厂商实现了memtrack模块,meminfo则可以通过libmemtrack的调用获取一些跟GPU相关的内存使用信息。

graphics:

Memory Tracker(memtrack),是一个android_hardware层的库,不一样平台库的名称不一样,实现方式也有差别。

graphics memory分为五种类型数据:

  • MEMTRACK_TYPE_OTHER = 0,
  • MEMTRACK_TYPE_GL = 1,
  • MEMTRACK_TYPE_GRAPHICS = 2,
  • MEMTRACK_TYPE_MULTIMEDIA = 3,
  • MEMTRACK_TYPE_CAMERA = 4

以查看AOSP源码高通为例,memtrack_msm.so库作的事情很简单,根据上层传递的type读取对应节点,获取内存信息。

https://cs.android.com/android/platform/superproject/+/master:hardware/qcom/display/msm8960/libmemtrack/memtrack_msm.c

int msm_memtrack_get_memory(const struct memtrack_module *module, pid_t pid, int type, struct memtrack_record *records, size_t *num_records){ if (type == MEMTRACK_TYPE_GL || type == MEMTRACK_TYPE_GRAPHICS) { return kgsl_memtrack_get_memory(pid, type, records, num_records); } return -EINVAL;}MEMTRACK_TYPE_GL = GL mtrack,MEMTRACK_TYPE_GRAPHICS = EGL mstrack

  • EGL mtrack:

gralloc分配的内存,主要是窗口系统,SurfaceView/TextureView和其他的由gralloc分配的GraphicBuffer总和。

  • GL mtrack:

驱动上报的GL内存使用情况。 主要是GL texture大小,GL command buffer,固定的全局驱动程序RAM开销等的总和。

理解以上内存更多详细内容,我们需要了解更多的体系内容如:

  • Android 硬件视图渲染机制、如何管理你的 Graphics的?
  • Native、Bitmap 工作流程
  • WebView 工作流程
  • MediaPlay 工作流程
  • Android运行时ART加载类和方法的过程。
  • Linux 内核的虚拟内存管理机制
  • Thread 工作流程

评估应用程序内存影响

影响应用程序性能其他因素:

  • 应用使用场景:WebP动图、酷炫动效、 Webview,视频,预加载渲染,占用的内存就高:
  • 平台配置:手机的分辨率越高,相同图片占用的内存就越大。

图片

  • 设备内存压力:设备内存越紧张,越可能触发 GC,导致 App 占用内存比设备内存充裕时低:

图片

理解 OutOfMemoryError

图片

1.Bitmap

  • 3.0~7.0 Bitmap对象 和 像素数据 统一放到 Java Heap 中(Fresco匿名共享内存上分配)
  • 8.0以上 像素数据放到 Native 内存,Android 8.0 解决图片内存占用过多和图像绘制效率过慢,新增了 硬件位图 Hardware BitmapGlide 有支持使用需要注意)。

硬件位图仅在显存 (graphic memory) 里存储像素数据,并对图片仅在屏幕上绘制的场景做了优化。
优化Bitmap的使用、针对性及时分配,及时释放策略

图片

根据设备等级

  • 根据设备、设备等级,对于低端、设备定制小缓存与回收策略、图片质量565、控制webp动图等。

图片

Preloader

  • 统一占位图资源获取(减少重复Bitmap)RecyclerViewPreloader及 滑动时加载图片优化
  • 横向ViewPager 预加载优化
  • 底部RecycleView 优化
  • 大图浏览优化
  • 视频浏览预加载
  • 利用onRecyleView回收已经
  • 多tab页面复用RecyclerPool
  • 等参考Epoxy Image-Preloadinghttps://github.com/airbnb/epoxy/wiki/Image-Preloading

统一图片获取

  • 统一占位图资源获取(减少重复Bitmap)

Memory Leak

  • Android 组件泄露(阿钱已做兜底处理、TOP页面避免泄露)
  • 业务组件泄露
  • Native 内存泄露

Thread

  • 一般性线程池
  • 视频渲染问题
  • 第三方sdk问题
  • 框架内发生错误

Code

代码中的某些资源和库可能会在您不知情的情况下吞噬内存。APK 的总体大小(包括第三方库或嵌入式资源)可能会影响应用的内存消耗量。

  • 针对性加载代码和初始化
  • 缩减APK 大小

other

  • 引导页面、闪屏页面使用完及时释放
  • 销毁LRU不使用的tab页面
  • 业务缓存管理问题
  • 多进程

3.Monitor

  • ✅线程监控、发现内存爆炸上报
  • 🔲线程创建追踪hook(art hook,已实现存在兼容问题)
  • 🔲内存Dump(超过虚拟内存的 80%)->上传->分析内存对象占比
  • 🔲大图、重复Bitmap监控(hook bitmap创建监控、重复Bitmap 上报)
  • 🔲Bitmap 内存占比监控分析(不同场景)
  • ✅系统通知低内存监控
  • ✅内存详细监控
  • ✅OOM监控
  • 🔲深入分析监控具体.jar .so等code资源 长时间占用分析

这块主要记录我们治理OutOfMemoryError 的一些优化方式与监控方案。

总结

第一节我们学习了Android 内存管理机制的一些前置知识,内存管理技术、是如何影响我们的应用的,知道了Android 内存区域的划分及部分原理,我们就可以针对各个区域提出内存优化的思路来提高我们的内存使用效率。

最后是我们治理OutOfMemoryError 的一些优化方式与监控方案,这块通常是分析后根据实际情况制定优化方向及方案来进行的。