Android Bitmap在虚拟内存与物理内存空间分配特性

Android Bitmap在虚拟内存与物理内存空间分配特性

摘要:Android Bitmap内存分配需要虚拟地址空间连续,但通常不要求物理内存连续。普通Bitmap(如ARGB_8888)的像素数据来自Java堆或native堆,只需虚拟地址连续,物理页可分散。硬件相关Buffer(如GraphicBuffer)才可能需要物理连续内存。LruCache仅管理引用不影响内存连续性。内存不足时,可能因虚拟地址空间碎片化或进程内存限制导致OOM,与CMA通常无关。不同Android版本对Bitmap内存位置的策略差异会影响OOM表现。

Bitmap 的"像素内存"通常需要在进程虚拟地址空间里是一段连续的内存;但不一定要求在物理内存上连续。

这是理解 Bitmap、CMA、OOM 的关键区别。

1. 先区分两个"连续"

内存连续有两种含义:

1)虚拟地址连续

也就是在 App 进程看到的地址空间里是一段连续区域。

比如 Bitmap 像素区可能像这样:

复制代码
0x70000000 ~ 0x70A00000

长度约 10MB。

对于普通 Bitmap 像素 buffer 来说,通常需要一段连续的 虚拟地址空间

2)物理地址连续

也就是 RAM 里面真实的物理页也必须挨在一起:

复制代码
物理页 A、A+1、A+2、A+3...

这个要求更高,通常只有 DMA、Camera、Video、Display、部分 GraphicBuffer/HardwareBuffer 才需要。

普通 Java/Native 分配一般不要求物理连续。

2. 普通 Bitmap 是否需要物理连续?

一般不需要。

比如普通软件 Bitmap:

复制代码
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
BitmapFactory.decodeResource(...)
BitmapFactory.decodeFile(...)

它的像素内存通常来自:

  • Java heap;

  • native heap;

  • malloc;

  • mmap;

  • ashmem;

  • Android Runtime/Skia 管理的 native allocation。

这些分配通常只需要 虚拟地址连续,底层可以映射到离散的物理页。

也就是说,10MB Bitmap 在虚拟地址上可能连续,但物理内存上可能是很多分散的 4KB page

复制代码
虚拟地址连续:
[ page ][ page ][ page ][ page ][ page ] ...

物理地址可能分散:
PFN 1000, PFN 3500, PFN 900, PFN 12000 ...

所以普通 Bitmap 通常不依赖 CmaFree

3. 那为什么 Bitmap 还可能 OOM?

即使不需要物理连续,也可能失败,因为它需要满足这些条件:

1)进程可用内存够不够

例如 ARGB_8888:

复制代码
宽 × 高 × 4 bytes

一张 10MB Bitmap 大约就是:

复制代码
10 × 1024 × 1024 bytes

如果 App heap 或 native heap 空间不足,就可能 OOM。

2)虚拟地址空间是否够连续

在 64 位 App 中通常问题不大。

但在 32 位进程中,虚拟地址空间有限,且可能碎片化。

这时候即使系统总内存还有,也可能找不到一段足够大的连续虚拟地址空间,导致分配失败。

不过 10MB 通常不算特别大,除非进程内存已经非常碎片化或接近上限。

3)Android 版本差异

Bitmap 像素内存位置历史上有变化:

  • 早期 Android:Bitmap 像素可能在 native heap;

  • Android 3.0 ~ 7.x:更多计入 Dalvik/ART heap;

  • Android 8.0 以后:Bitmap pixel data 又更多放到 native heap,但会通过 native allocation 影响 GC。

所以 OOM 类型可能不同,但本质还是分配内存失败或触发进程限制。

4. 硬件 Bitmap 呢?

如果是:

复制代码
Bitmap.Config.HARDWARE

或者图形相关 buffer:

  • HardwareBuffer

  • GraphicBuffer

  • Surface buffer

  • Texture

  • Camera preview buffer

  • Video decoder buffer

那就不同了。

这些可能由 GPU/display/camera 等硬件使用,底层可能走:

  • dma-buf

  • ION

  • DMA heap

  • gralloc

  • CMA

  • carveout memory

这类 buffer 有时会要求:

  • 物理连续;

  • 或 IOMMU 可映射;

  • 或特定内存区域;

  • 或满足 stride/alignment;

  • 或符合硬件 DMA 约束。

这种场景下,CmaFree=0 就可能相关。

5. 放在 LruCache 里的 Bitmap 连续性有变化吗?

不会。

LruCache 只是保存 Bitmap 对象引用。

它不会改变 Bitmap 像素内存的分配方式。

也就是说:

复制代码
memoryCache.put(key, bitmap);

只是让 cache 强引用这个 Bitmap。

Bitmap 的像素内存早在创建/解码时就已经分配好了。

LruCache 影响的是:

  • 是否继续持有引用;

  • 何时按 LRU 淘汰;

  • 是否触发 GC 后释放。

它不影响底层像素 buffer 是不是物理连续。

6. 举个例子

假设创建一张普通 ARGB_8888 Bitmap:

复制代码
Bitmap bitmap = Bitmap.createBitmap(1600, 1600, Bitmap.Config.ARGB_8888);

占用约:

复制代码
1600 × 1600 × 4 = 10,240,000 bytes ≈ 9.77MB

它通常需要:

复制代码
一段约 10MB 的连续虚拟地址空间

但物理页可以分散。

然后放入:

复制代码
memoryCache.put("image", bitmap);

此时 LruCache 只是引用它。

如果 cache 超过 maxSize,会淘汰旧对象;如果没人引用旧 Bitmap,之后 GC/native free 才可能释放对应内存。

7. 回到问题

Bitmap 在内存空间里面是连续分配的吗?

结论是:

普通 Bitmap 的像素数据通常在虚拟地址空间中是连续的一块 buffer,但不要求物理内存连续。

所以:

  • 普通 Bitmap:一般与 CmaFree 没有直接关系;

  • 硬件 Bitmap / GraphicBuffer / Camera buffer:可能需要特殊连续内存或 IOMMU 映射,可能与 CMA 有关系;

  • LruCache:只负责持有引用和按容量淘汰,不决定内存连续性。

一句话概括:

Bitmap 创建时需要能分配出一块足够大的像素 buffer;对普通 Bitmap 来说,这块 buffer 主要是虚拟地址连续,不是物理地址连续。

推荐一个人工智能网站