位图和内存

位图对象通常是应用内存占用空间的最大单一贡献者。无论是应用图标、通知图片还是媒体内容,低效的位图处理都会很快导致内存不足 (OOM) 错误和系统范围的内存压力。

位图配置和像素数据

位图消耗的内存量主要取决于其尺寸(宽度 × 高度)和配置 (Bitmap.Config)。

该配置定义了用于表示每个像素的字节数:

配置 每像素字节数 说明
ALPHA_8 1 仅限 Alpha(透明度)通道。适用于遮罩。
RGB_565 2 红色(5 位)、绿色(6 位)、蓝色(5 位)。没有 Alpha 版。适用于不透明图片,对色彩保真度要求不高。
ARGB_8888 4 Alpha、Red、Green、Blue(各 8 位)。默认且最常见。
RGBA_F16 8 半精度浮点数。用于广色域和 HDR 内容。
HARDWARE 不适用 存储在显存 (gralloc/DMABuf) 中。请参阅硬件位图

记忆公式Memory (Bytes) = Width × Height × Bytes Per Pixel

例如,在 1080p 设备 (1920x1080) 上,ARGB_8888 中的全屏图片占用:1920 × 1080 × 4 字节 ≈ 8.3 MB。

堆位图与共享位图

堆位图(原生堆)

在现代 Android(8.0 及更高版本)中,位图像素数据存储在原生堆中,而只有一小部分封装容器对象驻留在 Java 堆中。

当应用需要呈现图片时,通常会先将压缩的图片文件解码为位图,然后将其存储在堆中。

共享位图 (ashmem/memfd)

当位图在进程之间传输时(例如,通过 Binder 传输到 SystemUI 以用于通知),Android 会使用共享内存ashmemmemfd)来避免复制像素数据。

可以通过调用 Bitmap.asShared()Bitmap 实例显式复制到共享内存,也可以在将 Bitmap 放入 Parcel 中(通常是通过将 Bitmap 添加到 Parcelable(例如 Bundle)中)并通过 Binder IPC 发送时隐式复制。

通过 Binder IPC 发送共享位图时,像素数据本身不会被复制,而是将引用共享内存区域的文件描述符复制到接收方进程。底层内存区域可能在多个进程之间共享,并且只有在引用它的所有文件描述符都已关闭后才会释放。

可变位图与不可变位图

  • 可变位图:创建后可以修改(例如,通过 Canvas)。它们始终需要自己的私有内存分配。如果复制的是可变 Bitmap,则必须进行深层复制(所有像素数据的第二个副本)。
  • 不可变的位图:无法更改。这样一来,便可进行优化,例如在不同的 Bitmap 实例之间共享相同的底层内存缓冲区。从 APK 资源 (BitmapFactory) 加载的位图通常是不可变的。

高效的位图处理

位图池化和重用

频繁分配和释放位图会导致分配抖动,从而迫使 GC 不断运行。常见的图片加载库使用 Bitmap 池

Google 建议使用 Glide 作为基于 Java 的应用的解决方案,并使用 Coil 作为基于 Kotlin 的应用的解决方案(尤其是在使用 Jetpack Compose 时)。

当不再需要位图时,应用会调用 bitmap.recycle() 或将其返回到池中,而不是让 GC 来处理它。下次需要具有相同尺寸和配置的位图时,对象池会提供现有缓冲区,从而避免进行新的分配。

硬件位图

Bitmap.Config.HARDWARE 允许您将像素数据直接存储在图形内存 (DMABuf) 中。

  • 优点
    • 节省内存:不使用应用或原生堆,而是使用 GPU 内存。无论如何,应用界面中显示的位图通常都需要复制到 GPU 内存,因此这可以节省复制操作和额外的内存开销。
    • 性能:由于数据已在 GPU 上,因此绘制速度非常快。
  • 缺点
    • 不可变:硬件位图无法修改。
    • 回读速度慢:从 CPU(例如 getPixel())访问像素的开销非常大。
    • 归因:在 AHAT 等标准工具中更难跟踪(请参阅下文)。

实操练习:探索位图

我们将使用 BitmapLab 示例应用来探索这些概念。

1. 使用 dumpsys meminfo 进行衡量

启动 BitmapLab,然后点按 ALLOCATE 10MB ARGB_8888。然后运行以下命令:

adb shell dumpsys meminfo -s com.android.bitmaplab

在较新的 Android 版本中,找到原生分配部分。与通用的应用摘要相比,这些属性可为位图提供更好的归因:

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • 位图 (malloced):在进程的原生堆中分配的位图。这是 Android 8.0 及更高版本中大多数标准位图的存储位置。
  • 位图(非 malloced):使用专用内存(例如硬件位图共享位图 [通过 ashmemmemfd])的位图。

如果您在 BitmapLab 中分配了 Shared Bitmap,您会看到它反映在 Bitmap (nonmalloced) 中:

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240
Bitmap (nonmalloced):        1                          10240  # <--- Shared Bitmap!

共享位图跟踪

在某些 Android 版本和内核配置中,dumpsys meminfo 还可为通过文件描述符映射到进程地址空间的位图提供高分辨率跟踪。

默认情况下,共享位图使用通用名称(“位图”)。如需启用详细的归因和唯一的位图跟踪(识别不同进程之间的共享位图),您必须启用以下系统属性:

adb shell setprop debug.hwui.bitmap_ashmem_long_name true

启用后,/proc/<pid>/smaps 中的 ashmem 区域将具有更具描述性的名称。meminfo 将会利用这一点,结果将如下所示:

 Shared Bitmaps
                         Count                       Size(KB)
                        ------                         ------
              Mapped:        1                          10240
              Unique:        1                          10240
  • 已映射:所有与位图相关的内存映射的总大小。
  • 唯一:仅考虑唯一项的位图大小(即,如果多个映射指向同一底层共享位图像素数据,则只计数一次)。

2. AHAT 中的位图

AHAT 可为位图提供出色的可视化效果。

  1. BitmapLab 中,分配一些位图。
  2. 使用 -b 标志捕获堆转储(以包含原生位图数据):

    adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof
    adb pull /data/local/tmp/bitmaps.hprof .
    ahat bitmaps.hprof
    
  3. 打开 localhost:7100,然后在边栏中查找 Bitmaps 链接,或搜索 Bitmap 类。

  4. AHAT 实际上会在浏览器中渲染位图,从而轻松识别哪些图片占用了大量内存。

显示渲染位图的 AHAT

3. Perfetto 中的位图轨道

Perfetto 可以跟踪一段时间内的位图分配和计数。当为特定应用启用 gfx atrace 类别时,Android 框架会发出这些计数器。

  1. 开始跟踪。您必须添加 gfx 类别,并使用 -a 标志指定目标应用包:

    external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \
        -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplab
    
  2. BitmapLab 中,反复点按 AllocateClear 按钮。

  3. 同时点按 Parcel/Unparcel Bitmap

  4. ui.perfetto.dev 中分析轨迹。

com.android.bitmaplab 的进程部分,您将看到: * 位图数量:显示有效位图数量的计数器。 * 位图内存:显示位图使用的总字节数的计数器。

高级切片 (Perfetto SDK)

BitmapLab 还使用 Perfetto SDK 为位图操作发出高级切片。在轨迹中搜索 BitmapLab_,以查找以下内容: * BitmapLab_parcelUnparcel:涵盖打包和解包逻辑的切片。 * BitmapLab_postNotification:涵盖通知发布流程的 slice。

跟踪通知流程

当您点按 Post Notification 时,应用会创建一个包含当前位图的通知并将其发送到系统。负责此操作的框架代码会发出 Perfetto slice,其中包含连接序列化(将位图写入 Parcel 以通过 Binder IPC 发送)和反序列化(从接收端的 Parcel 读取位图)的流事件。

在下面的屏幕截图中,您可以看到应用将大型位图打包,以便在 Binder 交易中使用,从而发布通知,以及在 system_server 进程中进行相应的解包。

Perfetto 显示了从 BitmapLab 到 system_server 的流程(通过通知)

使用 Perfetto,您甚至可以跟踪同一通知位图在线程和进程之间的进一步传播,例如从 system_server(实现 INotificationManager Binder 服务器)中的 binder 线程传播到 system_server 工作线程,然后这些工作线程可能会将同一位图转发到 com.android.systemui 以在通知阴影中显示。

系统应用面临的挑战

SystemUI(通知)和 Launcher 等系统应用面临着独特的挑战:

  1. 无界限的内容:通知和 widget 的数量可能非常多。如果每个对象都包含一个大型位图,系统很快就会出现内存不足的情况。
  2. 重复:同一应用图标可能同时存在于启动器的缓存、SystemUI 的通知区域和“设置”应用中。
  3. 通过硬件缓冲区共享:为了缓解这一问题,系统组件正在转向集中式“图像分流”服务,该服务可在进程之间共享 HardwareBuffer 实例。
  4. DMABuf 归因:硬件位图可节省堆空间,但会使用 DMABuf 内存,而标准内存工具很难将 DMABuf 内存归因于特定进程。

    使用 adb shell dmabuf_dump 查看系统范围内的 DMABuf 分配。此工具可提供每个进程的缓冲区细分:

     droid.bitmaplab:19562
                      Name              Rss              Pss         nr_procs            Inode               Exporter
                 <unknown>          3840 kB          1280 kB                3             3397              virtio_gpu
                    system            12 kB             4 kB                3             3398                  system
                 <unknown>          3840 kB          1920 kB                2             3399              virtio_gpu
                    system            12 kB             6 kB                2             3400                  system
             PROCESS TOTAL         11556 kB          5136 kB
    
    • RSS:如果缓冲区已映射到进程中,则为缓冲区的总大小。
    • Pss:比例大小(RSS 除以共享缓冲区的进程数)。这是会计方面的最佳指标。
    • nr_procs:当前持有此缓冲区的进程数。
    • 导出器:分配缓冲区的驱动程序(例如,Cuttlefish 上的 virtio_gpu 或硬件上特定于供应商的 Ion/DMA-BUF 堆)。

    您还可以使用 adb shell dmabuf_dump -b 来获取所有缓冲区的摘要和整个系统的 DMA-BUF 总用量。


← Java | ↑ 上 | 原生 →