Android Bitmap屏显后Native堆占用以及GPU GraphicBuffer分布统计量
摘要:图片显示后内存占用主要由解码后的像素尺寸决定,而非原始文件大小。以400x400 ARGB_8888格式为例,内存分布为:1)Native堆存储像素数据约0.64MB;2)GPU纹理内存约0.64MB;3)少量Java对象和元数据。稳定显示后总增量约1.28MB。硬件加速下,CPU和GPU各存一份数据,其中Native堆归入内存统计,GPU内存计入Graphics/GL跟踪项。关键点:解码时应直接缩小尺寸,避免先解码大图再缩放;ImageView的缩放不会减少实际内存占用;使用HARDWARE配置可减少CPU副本但限制操作。
URI 原始文件大小 ≠ Bitmap 内存大小 Bitmap 内存大小 ≈ 宽 × 高 × 每像素字节数 ImageView.setImageBitmap() 本身通常不复制 Bitmap 像素 真正显示到屏幕时,硬件加速渲染会把 Bitmap 上传成 GPU texture 所以稳定显示后,通常会同时存在: 1 份 CPU/native 侧 Bitmap 像素内存 1 份 GPU 侧纹理内存 若干 Java/native 小对象和渲染缓存1. 先算 Bitmap 本体大小
假设原图是:
3000 x 4000通过BitmapFactory按照:
400 x 400解码后形成 Bitmap。
如果最终 Bitmap 是常见的:
Bitmap.Config.ARGB_8888那么每个像素 4 字节:
400 × 400 × 4 = 640,000 bytes换算:
约 625 KiB 约 0.61 MiB 约 0.64 MB所以这个 400x400 的 ARGB_8888 Bitmap 本体不是 3MB,而是约:
0.64 MB如果是其他格式:
Bitmap Config | 每像素字节数 | 400x400 Bitmap 大小 |
|---|---|---|
| 4 bytes | 640,000 bytes,约 0.61 MiB |
| 2 bytes | 320,000 bytes,约 0.31 MiB |
| 1 byte | 160,000 bytes,约 0.15 MiB |
| 8 bytes | 1,280,000 bytes,约 1.22 MiB |
下面默认按最常见的:
ARGB_8888来分析。
2. 稳定显示后,总体大概占多少?
在正常 Android 硬件加速渲染场景下:
imageView.setImageBitmap(bitmap)然后这个 ImageView 被绘制到屏幕上,通常会有:
CPU/native 侧 Bitmap 像素内存:约 0.64 MB GPU 侧 texture 纹理内存:约 0.64 MB Java 对象/Drawable/View 引用:很小,通常 KB 级 RenderNode/DisplayList/Skia/HWUI 元数据:小量 native所以这个单张 400x400 ARGB_8888 图片的稳定态增量内存大致是:
约 0.64 MB native Bitmap + 约 0.64 MB GPU texture + 少量 Java/native 元数据 ≈ 1.28 MB 左右更准确一点:
约 1.2 ~ 1.5 MB如果考虑 GPU 对齐、纹理缓存、allocator 开销,可能略高。
3. 分布拆解
3.1 URI 原始媒体文件
原始文件是:
3000 x 4000但如果它是 JPEG/PNG/WebP 文件,磁盘上的文件大小是压缩后的大小。
例如:
3000x4000 JPEG 可能是 1MB、3MB、8MB这不等于 Bitmap 内存。URI 指向的文件通常在:
/data/media/... MediaStore ContentProvider解码时会通过:
ContentResolver.openInputStream(uri) BitmapFactory.decodeStream(...)读取数据。这部分内存包括:
InputStream buffer Binder/ContentProvider 相关小对象 Skia 解码临时 buffer 系统 page cache其中:
文件 page cache 通常不算作 App 私有 Java/Native Heap如果 App 没有 mmap 整个文件,而只是流式读取,那么原始压缩文件本身通常不会以完整大小记到 App PSS 里。
3.2 BitmapFactory 解码阶段
BitmapFactory底层主要走 Skia 解码。
解码过程中可能有:
Java 层 Bitmap 对象 native 层 SkBitmap / PixelRef / pixel buffer decoder 临时 buffer InputStream buffer 颜色空间 / ICC profile / row buffer 等临时 native 内存最终产物是一个 Bitmap。在现代 Android 上,Bitmap 的像素内存主要在:
native heap而不是 Java heap。
Java 层的Bitmap对象只是一个 wrapper,比较小。
最终 Bitmap 像素大小:
rowBytes × height通常近似:
width × height × bytesPerPixel例子:
400 × 400 × 4 = 640,000 bytes所以 native 侧稳定保留:
约 0.64 MB3.3 ImageView.setImageBitmap 阶段
调用:
imageView.setImageBitmap(bitmap)内部大致是:
ImageView 持有这个 Bitmap 一般会包装成 BitmapDrawable 触发 invalidate 等待下一帧绘制重要点:
setImageBitmap()本身通常不会复制一份 Bitmap 像素数据。
所以在这个阶段,主要增加的是:
ImageView 对 Bitmap 的引用 BitmapDrawable 对象 少量 Java/native 状态这部分一般是:
KB 级别不是 MB 级。
3.4 真正绘制到屏幕阶段
当 ViewRoot 开始 draw,硬件加速开启时:
ImageView.draw() BitmapDrawable.draw() Canvas.drawBitmap() HWUI / RenderThread / Skia GPU backend此时系统会把 CPU/native 侧 Bitmap 上传到 GPU,形成 GPU texture。
对于 400x400 ARGB_8888:
GPU texture 大小 ≈ 400 × 400 × 4 = 640,000 bytes也就是:
约 0.64 MB实际可能因为:
纹理行对齐 tile 对齐 GPU allocator 对齐 驱动内部 metadata 是否生成 mipmap 纹理缓存策略略微高一点。
但一般可粗略按:
GPU texture ≈ Bitmap 像素大小来估算。
4. 所以最终这条链路的内存分布大概是?
以:
400x400 ARGB_8888为例,稳定显示后:
部分 | 大小估算 | 归属 |
|---|---|---|
Java
对象 | 很小,几十到几百 bytes 级 | Java Heap |
/
/ 引用 | KB 级 | Java Heap |
Bitmap 像素内存 | 约 0.64 MB | Native Heap |
Skia / decoder / RenderNode 元数据 | KB 到几十 KB | Native Heap / Private Other |
GPU texture | 约 0.64 MB | Graphics / GL mtrack / GPU memory |
Window Surface buffer | 不是这张图片独占,通常已存在 | Graphics |
原始 URI 文件 page cache | 通常不算 App 私有 heap | System page cache / file cache |
所以单张图片的增量稳定态可以粗略写成:
Native Heap: Bitmap pixels ≈ 0.64 MB GPU / Graphics: Texture ≈ 0.64 MB Java Heap: 很小,通常 KB 级 Total incremental: ≈ 1.28 MB + 少量对象/对齐/缓存开销5. 为什么会有 native 一份,又有 GPU 一份?
因为 CPU 和 GPU 使用的内存空间不同。
5.1 Bitmap 解码后的内存
BitmapFactory解码出来的是 CPU 可访问的像素数据。
App 可能会对它做:
bitmap.getPixel() bitmap.copy() canvas.drawBitmap()这类操作需要 CPU 可读的像素内存。这部分在现代 Android 上通常位于:
native heap5.2 屏幕显示需要 GPU texture
Android 正常 UI 渲染是硬件加速的。
ImageView 里的 Bitmap 最终需要被 GPU 采样、缩放、混合、合成。
GPU 不能直接高效使用普通 native heap 中的像素内存,通常需要上传成:
GPU texture所以会形成第二份:
GPU 侧纹理内存于是稳定显示时常见就是:
CPU/native Bitmap 一份 GPU texture 一份这就是为什么在手机屏幕上肉眼看到一个图片,实际内存往往接近:
Bitmap size × 2甚至更多。
6. ImageView 缩放会不会改变 Bitmap 内存?
一般不会。
例如:
Bitmap 是 400x400 ImageView 显示成 100x100Bitmap native 内存仍然是:
400 × 400 × 4GPU texture 通常也仍然是:
400 × 400 × 4ImageView 的scaleType只是改变绘制矩阵,不会自动减少 Bitmap 内存。
如果希望减少内存,需要在解码阶段就降低尺寸,例如:
decode 到 100x100而不是:
decode 到 400x400,然后显示成 100x1007. 原图 3000x4000 对最终内存有什么影响?
如果确实最终只解码成:
400x400那么最终稳定态主要由:
400x400决定,而不是:
3000x4000但是有几个例外。
7.1 如果中间曾经解码过原图
如果代码先解码完整原图:
3000 × 4000 × 4 = 48,000,000 bytes约:
45.8 MiB然后再 scale 到 400x400,那么短时间内可能存在:
原图 Bitmap 约 45.8 MiB + 缩略图 Bitmap 约 0.64 MB + GPU texture 约 0.64 MB这会非常浪费。
所以图库里必须避免:
先 decode full size,再 scale而应该用:
inSampleSize ImageDecoder targetSize 缩略图 API7.2 BitmapFactory 不一定能直接任意解码成 400x400
传统BitmapFactory.Options.inSampleSize通常按采样倍数缩小,尤其历史上常见是 2 的幂采样。
例如原图:
3000x4000用inSampleSize=8可能得到大约:
375x500如果最终需要 400x400,可能还会有 crop/scale。
如果内部流程是:
decode sampled bitmap 375x500 再 createScaledBitmap/crop 到 400x400那么峰值内存可能是:
sampled bitmap + final bitmap + GPU texture但稳定态如果只保留 final 400x400,则还是前面的估算。
8. OS 会把这些内存统计到哪里?
这部分要看使用什么工具。
下面以常见的:
adb shell dumpsys meminfo <package>和系统 PSS 统计为准。
8.1 Java Heap
归属内容:
Bitmap Java wrapper ImageView BitmapDrawable Drawable state Uri/String/Options 等 Java 对象 临时 byte[] buffer,如果你自己分配了这部分很小。
通常不会看到:
Java Heap 增加 0.64 MB因为 Bitmap 像素不在 Java byte[] 里。
8.2 Native Heap
归属内容:
Bitmap 像素内存 Skia native 对象 BitmapFactory decoder 临时 native buffer malloc 分配对于 400x400 ARGB_8888,稳定态最主要是:
Bitmap pixels ≈ 0.64 MB在dumpsys meminfo里通常会进入:
Native Heap或者厂商/Android 版本会在 Bitmap 相关统计里显示 Bitmap 占用。
所以可能看到:
Native Heap PSS / Private Dirty 上升 Bitmap memory 上升8.3 Graphics / GPU
归属内容:
GPU texture HWUI texture cache RenderThread/HWUI caches EGL/GL mtrack GraphicBuffer / Surface buffer这部分在dumpsys meminfo里通常体现为:
Graphics EGL mtrack GL mtrack Gfx dev不同平台名字会略有差异。 400x400 ARGB_8888 Bitmap 显示后,可能让 Graphics 增加约:
0.64 MB但实际可能更高,因为:
纹理对齐 HWUI 缓存 Surface buffer Render target buffer 厂商 GPU 驱动统计方式8.4 Surface / Window Buffer
这个要特别说明。
一个 App 窗口本身通常有 Surface buffer,例如:
1080 × 2400 × 4 bytes ≈ 9.9 MB如果双缓冲/三缓冲:
约 20 MB ~ 30 MB这部分是整个窗口的显示缓冲,不是某一张 ImageView 图片独占。
如果你的 App 已经在前台显示,这些 Surface buffer 本来就存在。
此时新增一张图片,主要增量是:
GPU texture不是重新增加整个窗口 buffer。
但如果这是 App 第一次启动、第一次创建窗口,那么你会同时看到:
Surface buffer / Graphics 明显增加这部分不能全部归因于这张 Bitmap。
8.5 PSS 统计
PSS 的统计会分散在:
Java Heap PSS Native Heap PSS Graphics / GL / EGL mtrack Private Other System需要注意:
GPU 内存不一定都能在
/proc/<pid>/smaps的 PSS 里完整体现,很多时候依赖 memtrack HAL,以Graphics、GL mtrack、EGL mtrack等形式展示。
所以在dumpsys meminfo里看到的总 PSS,可能和真实 GPU 物理占用并不完全一一对应。
9. 一张图总结
场景:
URI 原图:3000x4000 解码结果:400x400 ARGB_8888 Bitmap ImageView.setImageBitmap() 最终显示稳定态大致是:
Java Heap: Bitmap wrapper + ImageView/Drawable ≈ KB 级 Native Heap: Bitmap pixel buffer 400 × 400 × 4 ≈ 0.64 MB GPU / Graphics: GPU texture 400 × 400 × 4 ≈ 0.64 MB Total incremental: ≈ 1.28 MB + 少量额外开销如果用公式:
CPU Bitmap = width × height × bytesPerPixel GPU Texture ≈ width × height × bytesPerPixel Total ≈ 2 × width × height × bytesPerPixel对于 400x400 ARGB_8888:
Total ≈ 2 × 400 × 400 × 4 ≈ 1,280,000 bytes ≈ 1.22 MiB10. 特殊情况:Bitmap.Config.HARDWARE
如果使用的是:
Bitmap.Config.HARDWARE或者某些图片库使用了 hardware bitmap,那么情况会变。
Hardware Bitmap 的像素数据更偏向 GPU/GraphicBuffer 侧,CPU 通常不可直接访问。
特点:
Native Heap 中可能不会有完整 CPU 像素副本 Graphics / GPU / dmabuf / GraphicBuffer 占用会上升 不能直接修改像素 部分 Canvas 操作受限这种情况下可能更接近:
GPU/GraphicBuffer 一份 Native/Java wrapper 少量而不是普通 software Bitmap 的:
native 一份 + GPU 一份但普通BitmapFactory.decodeStream()默认更常见还是 software Bitmap,尤其没有明确设置Bitmap.Config.HARDWARE时。
12. 对内存问题的实际意义
如果上层应用同时保留很多 Bitmap,例如:
120 张首批缩略图 每张 400x400 ARGB_8888粗略估算:
单张 native bitmap ≈ 0.64 MB 单张 GPU texture ≈ 0.64 MB 单张合计 ≈ 1.28 MB 120 张 ≈ 153.6 MB当然实际图库缩略图可能小于 400x400,图片库也有缓存淘汰,但这个量级解释了为什么:
Native Heap Graphics PSS会随着图片展示明显上涨。
总结:
在现代 Android 上,一个
400x400 ARGB_8888Bitmap 通过ImageView.setImageBitmap()显示后,稳定态通常大约占0.64MB Native Bitmap + 0.64MB GPU Texture,合计约1.28MB,再加少量 Java/native 元数据。Native Bitmap 通常归到Native Heap/Bitmap,GPU texture 和 Surface/GraphicBuffer 归到Graphics / GL mtrack / EGL mtrack / Gfx dev等统计中。原始 3000x4000 文件大小本身不决定最终 Bitmap 内存,真正决定的是解码后的像素尺寸和像素格式。
AI智能网站