Android图片解码普通/HARDWARE Bitmap到GPU绘制最终上屏显示链路
摘要:Android中普通Bitmap和HARDWARE Bitmap从解码到屏幕显示的完整链路及核心区别。普通Bitmap解码后在CPU内存存储像素数据,绘制时需上传GPU纹理,适合需要像素处理的场景;HARDWARE Bitmap则将像素直接存入GPU/硬件缓冲,减少纹理上传开销,但不可修改像素且不兼容软件渲染。文章详细比较了两者在解码、绘制阶段的技术实现差异,列举了HARDWARE Bitmap的使用限制和适用场景,最后总结了两者的核心区别:普通Bitmap是"CPU内存像素图",HARDWARE Bitmap是"GPU侧图片资源"。
一、普通 Bitmap 从文件到屏幕的完整链路
以这段代码为例:
val bitmap = BitmapFactory.decodeFile(path)
imageView.setImageBitmap(bitmap)
整体链路可以分成几个阶段:
图片文件 JPEG/PNG/WebP
↓
解码器解码
↓
Bitmap 像素内存
↓
ImageView 持有 BitmapDrawable
↓
View 绘制流程
↓
Canvas 记录绘制指令
↓
RenderThread / Skia / GPU
↓
Surface Buffer
↓
SurfaceFlinger 合成
↓
Display 硬件扫描显示
↓
人眼看到
1. 图片文件阶段
图片文件通常是压缩格式,例如:
-
JPEG
-
PNG
-
WebP
-
HEIF
-
AVIF
这些文件本身不是屏幕可以直接显示的像素矩阵,而是一种压缩编码后的数据。
例如 JPEG 文件里存的是经过 DCT、量化、熵编码之后的数据;PNG 里面是无损压缩后的像素数据。
2. 解码阶段:文件变成 Bitmap
调用:
BitmapFactory.decodeFile(path)
或者:
BitmapFactory.decodeStream(inputStream)
Android 底层通常会通过 Skia 图形库进行解码。
解码过程大致是:
读取文件字节
↓
识别图片格式
↓
解析图片头信息
↓
解压缩图片数据
↓
颜色转换 / Alpha 处理
↓
生成 Bitmap
解码完成后,得到的是一个 Bitmap 对象。
普通情况下,这个 Bitmap 会有一块可被 CPU 访问的像素内存,例如:
Bitmap 对象
↓
native 层像素缓冲区
↓
ARGB_8888 / RGB_565 / RGBA_F16 等格式
常见配置:
Bitmap.Config.ARGB_8888
Bitmap.Config.RGB_565
Bitmap.Config.RGBA_F16
Bitmap.Config.ALPHA_8
例如 ARGB_8888:
每个像素 4 字节
A 8 bit
R 8 bit
G 8 bit
B 8 bit
如果图片是 1000 x 1000,ARGB_8888 的内存大约是:
1000 * 1000 * 4 = 4 MB
注意,这里的 Bitmap 是"解码后的像素数据",不是原始 JPEG/PNG 文件大小。
3. setImageBitmap:Bitmap 被包装进 Drawable
调用:
imageView.setImageBitmap(bitmap)
内部大致等价于:
setImageDrawable(new BitmapDrawable(getResources(), bitmap));
也就是说,ImageView 通常不会直接绘制 Bitmap,而是持有一个 Drawable,比如 BitmapDrawable。
链路变成:
Bitmap
↓
BitmapDrawable
↓
ImageView.mDrawable
然后 ImageView 会触发:
requestLayout()
invalidate()
具体取决于图片尺寸、ScaleType、布局状态等。
invalidate() 的意思是:
这个 View 需要重新绘制。
但注意,调用 setImageBitmap() 之后,图片并不会立刻显示在屏幕上,而是等下一帧 VSYNC 到来后,Android 的绘制系统统一处理。
4. View 绘制流程
Android 的 UI 绘制一般由 ViewRootImpl 驱动。
一帧绘制大致包括三大阶段:
measure
layout
draw
也就是:
测量 View 大小
↓
确定 View 位置
↓
绘制 View 内容
对于已经布局好的 ImageView,通常主要发生的是 draw 阶段。
绘制调用链大致是:
ViewRootImpl.performTraversals()
↓
View.draw()
↓
ImageView.onDraw(Canvas canvas)
↓
Drawable.draw(canvas)
↓
BitmapDrawable.draw(canvas)
↓
canvas.drawBitmap(...)
ImageView 还会根据自己的属性处理图片如何显示,例如:
android:scaleType="centerCrop"
android:scaleType="fitCenter"
android:scaleType="centerInside"
android:scaleType="matrix"
所以真实绘制时,可能会有矩阵变换:
Bitmap 原始尺寸
↓
ImageView 根据 ScaleType 计算 Matrix
↓
缩放 / 平移 / 裁剪
↓
绘制到 Canvas
例如 centerCrop:
图片被等比放大/缩小
填满 ImageView
多余部分被裁剪
5. Canvas:软件 Canvas 和硬件加速 Canvas
这里要区分两种情况:
情况一:软件绘制
如果硬件加速关闭,或者绘制到一个普通软件 Bitmap 上,那么 Canvas 是软件 Canvas。
绘制大致是:
CPU 读取 Bitmap 像素
↓
CPU 做缩放 / 混合 / 裁剪
↓
写入目标像素缓冲区
这种方式完全依赖 CPU。
情况二:硬件加速绘制,常见情况
现代 Android App 默认基本都是硬件加速绘制。
这时 Canvas 不是简单地立即把像素画到内存里,而是会把绘制操作记录成图形指令。
大概是:
ImageView.onDraw()
↓
canvas.drawBitmap(bitmap, matrix, paint)
↓
记录 drawBitmap 指令到 RenderNode / DisplayList
UI 线程主要负责:
构建 View 树
执行 onDraw()
记录绘制命令
真正执行 GPU 绘制的,很多工作会交给:
RenderThread
↓
Skia
↓
OpenGL ES / Vulkan
↓
GPU
6. Bitmap 上传为 GPU Texture
如果是普通 Bitmap,例如 ARGB_8888,它的像素数据一般在 CPU 可访问内存中。
当硬件加速 Canvas 需要绘制它时,系统需要让 GPU 能访问这张图片。通常会发生:
CPU Bitmap 像素内存
↓
上传到 GPU
↓
变成 GPU Texture
↓
GPU 使用这个 Texture 绘制矩形
也就是说:
Bitmap
↓
Texture
↓
绘制到 Surface Buffer
这个上传过程可能比较耗时,尤其是大图首次绘制时。
Android/Skia/RenderThread 会做缓存。通常同一个 Bitmap 后续再次绘制时,不一定每次都重新上传,可能复用已有的 GPU 纹理缓存。
但如果:
-
Bitmap 被修改了;
-
Bitmap 被回收了;
-
纹理缓存被清理;
-
内存压力较大;
-
Bitmap 太大;
那么可能又要重新上传。
7. GPU 绘制到 App 的 Surface
每个 Activity 窗口背后一般对应一个 Surface。
App 绘制时不是直接画到屏幕,而是画到一块图形缓冲区中。
链路大概是:
GPU 执行绘制命令
↓
把结果画到 App 的 Surface Buffer
这个 Buffer 通常由 BufferQueue 管理,可能是双缓冲或三缓冲。
App 生产 Buffer
↓
BufferQueue
↓
SurfaceFlinger 消费 Buffer
8. SurfaceFlinger 合成
Android 系统中负责合成各个窗口画面的核心服务是:
SurfaceFlinger
它会拿到不同应用、系统栏、输入法、弹窗等 Surface 的 Buffer,然后合成最终画面。
例如:
你的 App Surface
状态栏 Surface
导航栏 Surface
输入法 Surface
Toast / Popup Surface
↓
SurfaceFlinger 合成
↓
最终帧
合成可以由:
-
GPU
-
Hardware Composer,简称 HWC
-
Display Controller
共同完成。
9. 显示硬件输出到屏幕
最终合成好的帧会交给显示硬件。
显示屏按照刷新率扫描显示,例如:
60Hz:约 16.67ms 一帧
90Hz:约 11.11ms 一帧
120Hz:约 8.33ms 一帧
显示硬件将像素信号输出到屏幕面板上,屏幕发光,最后被人眼看到。
普通 Bitmap 的总体链路总结
JPEG/PNG/WebP 文件
↓ 解码
CPU 可访问 Bitmap 像素内存
↓ setImageBitmap
ImageView 持有 BitmapDrawable
↓ invalidate
ViewRootImpl 下一帧触发绘制
↓
ImageView.onDraw()
↓
Canvas.drawBitmap()
↓
硬件加速下记录绘制命令
↓
Bitmap 上传为 GPU Texture
↓
GPU 绘制到 App Surface Buffer
↓
SurfaceFlinger 合成
↓
屏幕显示
二、如果 Bitmap 是 HARDWARE 模式,有什么不同?
从 Android 8.0,API 26 开始,引入了:
Bitmap.Config.HARDWARE
例如:
val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.HARDWARE
}
val bitmap = BitmapFactory.decodeFile(path, options)
或者图片加载库,例如 Glide/Coil3,在某些条件下也会使用 Hardware Bitmap。
1. HARDWARE Bitmap 的核心特点
HARDWARE Bitmap 的关键点是:
像素数据不在普通 Java/native CPU 可直接访问的内存里,而是更接近 GPU 侧的图形缓冲资源。
可以简单理解为:
普通 Bitmap:
图片像素主要在 CPU 内存中,需要时上传到 GPU。
HARDWARE Bitmap:
图片像素已经在 GPU/图形硬件友好的内存中,绘制时可以更直接作为纹理使用。
底层通常与:
GraphicBuffer / AHardwareBuffer
这类硬件图形缓冲相关。
2. 解码阶段的不同
普通 Bitmap 解码大致是:
图片文件
↓
解码到 CPU 像素内存
↓
Bitmap
HARDWARE Bitmap 大致是:
图片文件
↓
解码
↓
放入硬件图形缓冲区
↓
Hardware Bitmap
从应用角度看,它还是一个 Bitmap 对象,但它背后的像素存储方式不同。
3. 绘制阶段的不同
普通 Bitmap 在硬件加速绘制时:
CPU Bitmap 像素
↓
上传到 GPU Texture
↓
GPU 绘制
HARDWARE Bitmap:
Hardware Bitmap / GraphicBuffer
↓
直接作为 GPU 可用资源
↓
GPU 绘制
所以它最大的优势是:
减少或避免普通 Bitmap 首次绘制时的 GPU 上传成本。
尤其是列表、瀑布流、大图显示等场景,Hardware Bitmap 可以降低 UI 线程或 RenderThread 的纹理上传压力。
4. ImageView 使用 HARDWARE Bitmap 时链路变化
代码层面看,还是:
imageView.setImageBitmap(bitmap)
ImageView 仍然是:
ImageView
↓
BitmapDrawable
↓
Bitmap
绘制调用链也类似:
ImageView.onDraw()
↓
BitmapDrawable.draw()
↓
Canvas.drawBitmap()
但是底层资源不同。
普通 Bitmap:
drawBitmap 时发现这是 CPU Bitmap
↓
需要上传或找到缓存的 GPU Texture
↓
绘制
HARDWARE Bitmap:
drawBitmap 时发现这是 Hardware Bitmap
↓
直接引用硬件缓冲/GPU 资源
↓
绘制
因此,Java/Kotlin 层的 API 使用方式差别不大,但底层绘制路径更偏 GPU 资源直接使用。
三、HARDWARE Bitmap 的限制
Bitmap.Config.HARDWARE 不是普通 Bitmap 的完全替代品,它有不少限制。
1. 不可修改
Hardware Bitmap 是不可变的。
bitmap.isMutable // false
不能对它执行像素修改操作。
例如下面这些操作不适合或会报错:
bitmap.setPixel(x, y, color)
bitmap.eraseColor(Color.RED)
因为应用不能直接写它的像素内存。
2. 不能直接读取像素
普通 Bitmap 可以:
bitmap.getPixel(x, y)
bitmap.getPixels(...)
copyPixelsToBuffer(...)
但 Hardware Bitmap 通常不允许 CPU 直接读取像素。
因为它的像素在硬件图形缓冲中,不是普通 CPU 内存。
如果尝试读取,可能会抛异常,例如:
IllegalStateException
或者类似:
unable to getPixels(), pixel access is not supported on Config#HARDWARE bitmaps
3. 不能画到软件 Canvas 上
这是一个非常重要的限制。
如果是硬件加速 Canvas,绘制 Hardware Bitmap 没问题。
但是如果你把 Hardware Bitmap 绘制到软件 Canvas,例如:
val target = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
val canvas = Canvas(target)
canvas.drawBitmap(hardwareBitmap, 0f, 0f, null)
这类场景可能会失败。
常见异常类似:
java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps
原因是:
软件 Canvas 依赖 CPU 访问源 Bitmap 像素
但 Hardware Bitmap 的像素不允许 CPU 直接访问
4. 不适合需要像素处理的场景
如果你要做这些操作,最好不要用 HARDWARE:
-
模糊处理;
-
灰度处理;
-
取主色;
-
圆角裁剪到新 Bitmap;
-
图片加水印;
-
截图合成;
-
自定义滤镜;
-
二维码识别;
-
OCR;
-
人脸识别前处理;
-
需要
getPixels(); -
需要
Canvas(bitmap)离屏绘制。
这些场景更适合使用:
Bitmap.Config.ARGB_8888
或者根据需求使用:
Bitmap.Config.RGBA_F16
5. 不能用于某些软件渲染路径
比如:
-
关闭硬件加速的 View;
-
RemoteViews 某些场景;
-
Notification 某些路径;
-
App Widget;
-
自定义软件合成;
-
生成缩略图时的软件 Canvas;
-
某些老设备或兼容路径。
在这些场景中,Hardware Bitmap 可能导致崩溃或显示失败。
四、普通 Bitmap 和 HARDWARE Bitmap 对比
| 对比项 | 普通 Bitmap,例如 ARGB_8888 | HARDWARE Bitmap |
|---|---|---|
| 像素位置 | CPU 可访问内存 | GPU/硬件图形缓冲 |
| 是否可变 | 可变或不可变都可以 | 一定不可变 |
能否 getPixel() |
可以 | 通常不可以 |
能否 setPixel() |
可变时可以 | 不可以 |
| 软件 Canvas 能否绘制 | 可以 | 通常不可以 |
| 硬件 Canvas 能否绘制 | 可以 | 可以 |
| 首次 GPU 绘制 | 可能需要上传纹理 | 通常更直接 |
| 适合场景 | 需要像素处理、编辑、软件合成 | 只展示图片 |
| 内存压力 | 占 CPU/native 内存,绘制时还可能有 GPU 纹理缓存 | 主要占图形/GPU 相关内存 |
| 是否适合 ImageView 展示 | 适合 | 很适合 |
| 是否适合图片编辑 | 适合 | 不适合 |
五、一个更形象的理解
普通 Bitmap 像这样:
图片存在 CPU 仓库里
要显示时,需要搬到 GPU 仓库
然后 GPU 负责画出来
HARDWARE Bitmap 像这样:
图片一开始就放在 GPU 更容易使用的仓库里
要显示时,GPU 可以直接拿来画
所以:
普通 Bitmap:更灵活,可读可写,适合处理。
HARDWARE Bitmap:更适合展示,减少上传成本,但不方便操作像素。
六、在 ImageView 中的具体区别
如果只是:
imageView.setImageBitmap(bitmap)
并且这个 ImageView 是正常硬件加速显示,那么:
普通 Bitmap
ImageView
↓
drawBitmap
↓
普通 Bitmap 上传/缓存为 GPU Texture
↓
GPU 绘制
HARDWARE Bitmap
ImageView
↓
drawBitmap
↓
直接使用硬件图形资源
↓
GPU 绘制
从业务代码看起来没什么区别,但性能特征不同。
七、HARDWARE Bitmap 的优点
1. 减少纹理上传开销
大图首次显示时,普通 Bitmap 可能有明显上传成本。
HARDWARE Bitmap 可以减少这部分成本。
2. 降低 Java/native 堆压力
普通 Bitmap 虽然像素内存很多时候在 native heap,但仍然属于进程内存压力的一部分。
Hardware Bitmap 更多使用图形缓冲资源,可以减少某些普通 Bitmap 内存路径的压力。
3. 更适合纯展示场景
例如:
-
Feed 图片流;
-
相册缩略图;
-
聊天图片;
-
Banner;
-
商品图片;
-
大图预览;
-
只展示不编辑的图片。
这些场景比较适合 Hardware Bitmap。
八、HARDWARE Bitmap 的缺点
1. 不可编辑
不能直接修改像素。
2. 不可读像素
不能方便地 getPixel() 或 getPixels()。
3. 软件渲染不兼容
绘制到软件 Canvas 时容易崩溃。
4. 可能增加 GPU/GraphicBuffer 压力
它不是没有成本,只是成本转移到了图形内存/硬件缓冲资源上。
如果大量使用 Hardware Bitmap,也可能造成:
显存 / GraphicBuffer / GPU memory 压力
在低端设备上尤其要注意。
九、常见问题
1. ImageView 设置 Bitmap 后,Bitmap 会立刻显示吗?
不会。
它通常会:
setImageBitmap()
↓
invalidate()
↓
等待下一次 VSYNC
↓
ViewRootImpl 执行绘制
↓
GPU 绘制
↓
SurfaceFlinger 合成
↓
屏幕刷新
2. Bitmap 解码出来多大,ImageView 就显示多大吗?
不一定。
显示大小取决于:
-
ImageView 自身大小;
-
scaleType; -
adjustViewBounds; -
layout params;
-
Drawable intrinsic size;
-
Matrix;
-
density 缩放。
例如一张 4000x3000 的图,放在 200x150 的 ImageView 里,可能只是缩放显示,但如果你没有采样解码,它在内存里仍然可能是 4000x3000 的完整 Bitmap。
3. ImageView 会自动帮我压缩 Bitmap 吗?
通常不会按你想象的方式自动压缩。
它会在绘制时缩放显示,但不会自动把大 Bitmap 重新解码成小 Bitmap。
所以加载大图时仍然应该使用:
BitmapFactory.Options.inSampleSize
或者使用 Glide、Coil、Picasso 等图片库根据目标尺寸解码。
4. Hardware Bitmap 可以转成普通 Bitmap 吗?
可以复制:
val softwareBitmap = hardwareBitmap.copy(Bitmap.Config.ARGB_8888, false)
但注意,这可能会触发从 GPU/硬件缓冲读回到 CPU 内存,成本可能较高。
十、简化总结
普通 Bitmap 链路:
压缩图片文件
↓
解码成 CPU 可访问像素
↓
ImageView 持有 BitmapDrawable
↓
View 绘制
↓
Bitmap 上传成 GPU Texture
↓
GPU 绘制到 Surface
↓
SurfaceFlinger 合成
↓
屏幕显示
HARDWARE Bitmap 链路:
压缩图片文件
↓
解码成硬件图形缓冲资源
↓
ImageView 持有 BitmapDrawable
↓
View 绘制
↓
GPU 直接使用该硬件资源绘制
↓
SurfaceFlinger 合成
↓
屏幕显示
核心区别一句话:
普通 Bitmap 更像"CPU 内存里的像素图",显示时需要上传给 GPU;HARDWARE Bitmap 更像"GPU/硬件侧的图片资源",更适合直接显示,但不能方便地读写像素,也不适合软件 Canvas。