Android图片解码普通/HARDWARE Bitmap到GPU绘制最终上屏显示链路

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。

推荐一个AI学习站点