Android显示系统RenderThread绘制HARDWARE/普通格式Bitmap与GPU与CPU处理机制
在 Android 里,Bitmap.Config.HARDWARE 的 Bitmap 和普通格式 Bitmap(如 ARGB_8888、RGB_565)最大的区别,不只是"格式不同",而是存放位置、可操作性、适用场景都不一样。普通 Bitmap:像素主要在 CPU 可访问内存 中,真正用于 GPU 渲染时,通常需要经过一次"上传到 GPU 纹理/图像资源"的过程。
Bitmap.Config.HARDWARE Bitmap:像素直接放在 硬件可直接使用的 buffer 里,绘制时通常不需要再走普通 Bitmap 那种 CPU->GPU 纹理上传,因此更适合"只显示不处理"的场景。
可以把它理解成:
- 普通 Bitmap:主要给 CPU 处理,适合读写像素、做图片加工
- HARDWARE Bitmap:主要给 GPU 显示,适合直接渲染到屏幕
1. 存储位置不同
普通 Bitmap
通常像素数据在 应用可访问的内存 中,CPU 能直接访问。这类 Bitmap 的像素数据一般在:
native/系统内存中
CPU 可访问
但如果最终要显示到屏幕,而 View 是硬件加速渲染的,那么 GPU 不能直接拿 Java/CPU 这边的像素内存来画,通常要先把这份像素数据变成 GPU 纹理资源。
HARDWARE Bitmap
像素数据通常在 GPU/GraphicBuffer 相关的硬件内存 中,主要为了高效显示。这会直接导致后面的"能做什么、不能做什么"差异。这种 Bitmap 的像素数据在:
GPU/图形系统可直接使用的硬件 buffer 中
应用侧不能像普通 Bitmap 那样随便读写。
2. 是否可变:HARDWARE 一定不可变
普通 Bitmap
可以是:
- mutable(可修改)
- immutable(不可修改)
比如可以创建一个可变 Bitmap,然后拿 Canvas 在上面画。
HARDWARE Bitmap
永远是不可变的(immutable)
也就是说不能:
- 往它上面画
- 修改其中某个像素
- 用它做"画布底图"进行编辑
3. 像素读写能力不同
这是最核心的区别。
普通 Bitmap
可以直接做很多 CPU 侧处理,比如:
- getPixel()
- setPixel()
- getPixels()
- copyPixelsToBuffer()
- copyPixelsFromBuffer()
- 图像滤镜
- 高斯模糊
- 像素遍历分析
- OpenCV / 自定义算法处理
HARDWARE Bitmap
这些操作通常受限或不支持,因为像素不在普通内存里,CPU 不能像普通 Bitmap 一样直接拿到整块像素数据。
所以如果要做:
- 二维码识别
- 图片压缩前处理
- 调色
- 马赛克
- 像素级特效
- 转 byte array / buffer 分析
那 HARDWARE Bitmap 不适合。
4. 绘制方式不同
普通 Bitmap
可以:
- 绘制到屏幕
- 绘制到另一个 Bitmap
- 作为 Canvas(Bitmap) 的目标
HARDWARE Bitmap
它更适合:
- 直接显示到 View / ImageView
- 配合硬件加速渲染
它的优势是:
- 显示时更高效
- 减少从 CPU 内存上传纹理到 GPU 的开销
- 更适合"只展示,不处理"的图片
但缺点是:
- 不适合软件 Canvas
- 某些场景下会报错,比如试图在软件渲染路径中使用它
5. 使用场景不同
适合 HARDWARE Bitmap 的场景
如果图片只是:
- 下载后展示
- 列表滚动显示
- 大图预览
- 不做任何像素处理
那么 HARDWARE 很合适,性能通常更好,内存使用策略也更偏向渲染优化。比如很多图片加载库在合适情况下会优先使用它来提升显示效率。
不适合 HARDWARE Bitmap 的场景
如果要:
- 对图片做变换/滤镜
- 读取像素
- 截图后继续处理
- 合成图片
- 生成缩略图并编辑
- 用 Palette 提取颜色
- 传给依赖 CPU 像素访问的库
那应使用普通 Bitmap,通常是:
- ARGB_8888
6. 创建和解码上的差异
普通 Bitmap
常见解码方式:
val Bitmap = BitmapFactory.decodeFile(path)
默认一般是普通配置(常见为 ARGB_8888)。
HARDWARE Bitmap
Android 8.0(API 26)开始支持:
Kotlin
val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.HARDWARE
}
val Bitmap = BitmapFactory.decodeFile(path, options)
但即使这样指定,系统也会根据条件决定是否真的返回 HARDWARE Bitmap。
7. 判断是不是 HARDWARE Bitmap
可以这样:
if (Bitmap.config == Bitmap.Config.HARDWARE) {
// 是硬件位图
}
8. 如果拿到 HARDWARE Bitmap 但又要处理怎么办?
常见做法是先拷贝成普通 Bitmap:
val softwareBitmap = Bitmap.copy(Bitmap.Config.ARGB_8888, true)
这样就能继续:
- 读写像素
- 用 Canvas 编辑
- 做滤镜和图像处理
注意:
- 这会产生一次额外内存分配
- 大图时要注意 OOM 和性能成本
9. 一句话总结两者处理方式的本质区别
普通 Bitmap
面向"处理"
- CPU 可访问
- 可读像素、可编辑
- 适合图像算法和二次加工
HARDWARE Bitmap
面向"显示"
- GPU 友好
- 不可修改
- 不适合像素级处理
- 更适合直接渲染到屏幕
10. 实际开发建议
如果需求是:
只显示图片
优先考虑 HARDWARE,可能更省事、显示更高效。
还要处理图片
不要用 HARDWARE,直接用:
Bitmap.Config.ARGB_8888
不确定后续会不会处理
也建议先别用 HARDWARE,否则后面还得 copy,一来一回反而增加开销。
11. 简单对照表
| 对比项 | 普通 Bitmap | HARDWARE Bitmap |
|---|---|---|
| 存储位置 | 应用可访问内存 | 硬件/GPU相关内存 |
| 是否可变 | 可变或不可变 | 永远不可变 |
| 像素访问 | 支持 | 一般不适合/受限 |
| Canvas 编辑 | 支持 | 不支持直接编辑 |
| 显示性能 | 普通 | 更适合硬件渲染 |
| 图像处理 | 很适合 | 不适合 |
| 推荐场景 | 编辑、算法、合成 | 纯展示 |
12. 普通 Bitmap 显示到屏幕,是否需要 RenderThread 上传?
通常需要,而且一般就发生在硬件加速渲染链路中。
但更准确地说,不是"每次显示都重新上传",而是:
- 第一次参与 GPU 绘制时,往往需要上传成纹理
- 后续如果纹理缓存还在,通常可以复用
- 如果缓存失效、纹理被回收,可能又会重新上传
更准确的流程
以一个普通 ARGB_8888 Bitmap 被 ImageView 显示为例,在硬件加速开启时,大致会经历:
- UI 线程记录绘制命令
- RenderThread / HWUI 消费这些绘制命令
- 当发现要绘制一个普通 Bitmap 时
- 如果 GPU 侧还没有对应纹理
- 就把 Bitmap 像素从 CPU 可访问内存上传到 GPU
- GPU 用这个纹理参与真正的渲染
- 最终交给 SurfaceFlinger 做合成并显示
13. HARDWARE Bitmap 是否就是避免了这一环节?
基本是的,但要加一个限定:它避免的是"普通 Bitmap 在首次 GPU 绘制时那种 CPU 内存 -> GPU 纹理资源的上传路径"。
因为 HARDWARE Bitmap 在创建/解码时,像素就已经放到 GPU/图形系统可直接使用的硬件资源里了。
所以在绘制时:
- 不需要把一份 CPU 侧 Bitmap 像素再额外上传成纹理
- 往往可以直接被渲染管线引用
这正是它性能上的重要价值之一。
14. 普通Bitmap一定Render Thread每帧上次吗?
真实实现里,不能简单理解成:
- 普通 Bitmap = 一定每帧上传
- HARDWARE Bitmap = 完全零成本直接显示
这两个说法都不够准确。
对普通 Bitmap 来说
它通常不是"每一帧都上传"。
更常见的是:
- 首次绘制时上传
- GPU 侧做缓存
- 后续重复绘制直接用缓存纹理
只有在这些情况下可能重新上传:
- Bitmap 变了
- 纹理缓存被驱逐
- 上下文丢失
- 内存紧张导致资源回收
所以普通 Bitmap 的问题主要是:
- 首次显示可能有上传开销
- CPU 和 GPU 侧可能各保留一份数据,造成更高内存占用
对 HARDWARE Bitmap 来说
它也不是"完全没有任何准备成本"。
因为在:
- 图片解码
- buffer 分配
- 图形资源创建
这些阶段也会有成本。
它只是把代价更早地转移到了"解码/创建资源"阶段,并且避免了后续普通 Bitmap 那种再上传一遍的路径。
15. RenderThread 在这里扮演什么角色?
在 Android 硬件加速渲染里:
- UI Thread:负责 View measure/layout/draw 命令录制
- RenderThread:负责执行这些渲染命令,和 GPU 交互
所以当在 View 树里画一个普通 Bitmap 时,真正跟 GPU 纹理创建、上传、绘制打交道的,通常是在 RenderThread/HWUI 这一侧。
因此可以理解为:
- 普通 Bitmap:RenderThread 常常要处理"纹理化"
- HARDWARE Bitmap:RenderThread 更多是"直接引用现成硬件资源"去画
16. 小结
可以把它理解成两种送货方式:
普通 Bitmap
货物存在 CPU 仓库里。
GPU 要用的时候,要先把这箱货搬运到 GPU 仓库 。
第一次搬运有成本,之后 GPU 仓库里有库存就能直接用了。
HARDWARE Bitmap
货物一开始就存放在 GPU/图形系统能直接使用的仓库里。
所以显示的时候,不需要再从 CPU 仓库搬一次。
这就是它能减少"上传环节"的原因。
17. 这带来的实际差异是什么?
普通 Bitmap
优点:
- 可读写
- 可做图像处理
- 适合编辑、滤镜、像素分析
代价:
- 用于 GPU 显示时通常要经历纹理上传
- 可能有 CPU + GPU 双份内存占用
HARDWARE Bitmap
优点:
- 更适合直接显示
- 避免普通 Bitmap 的首次纹理上传路径
- 常常能减少峰值内存和卡顿风险
代价:
- 不可变
- 不能方便地读写像素
- 某些 API 不支持
- 软件绘制链路不友好
18. 总结
普通格式 Bitmap 的像素数据通常位于 CPU 可访问内存中,在 Android 硬件加速渲染时,首次参与 GPU 绘制往往需要通过 RenderThread/HWUI 上传为 GPU 纹理;而 Bitmap.Config.HARDWARE 的 Bitmap 像素数据直接存储在图形系统可直接使用的硬件 buffer 中,绘制时通常可以避免这一步额外的纹理上传,从而更适合纯显示场景。
19.必须是硬件加速渲染场景
如果不是硬件加速,比如:
- 软件 Canvas
- 某些 CPU-only 处理链路
那么:
- 普通 Bitmap 仍然能正常工作
- HARDWARE Bitmap 反而不适合,甚至不能直接用
所以 HARDWARE Bitmap 的优势,主要体现在:
- ImageView
- RecyclerView 列表图片
- 大图浏览
- 纯展示型 UI
这些硬件加速显示场景里。
20. 误区
普通 Bitmap 的像素数据通常在 CPU 可访问内存中,在硬件加速渲染到屏幕时,首次绘制一般需要经过 RenderThread/HWUI 上传到 GPU 纹理;而 HARDWARE Bitmap 的像素数据直接位于硬件图形 buffer 中,因此通常可以避免这类额外上传过程。
很多人会误以为:
"HARDWARE Bitmap 就一定比普通 Bitmap 快"
其实不绝对。如果场景是:
- 要做图像处理
- 要频繁读取像素
- 要做 Bitmap 合成
- 要作为中间态反复加工
那 HARDWARE Bitmap 反而更麻烦,因为你最后又得 copy 回 ARGB_8888,得不偿失。
所以:
- 纯展示:HARDWARE 常常更好
- 要处理:普通 Bitmap 更合适
21.Coil/Glide等图片加载库为什么允许开发者禁用 Bitmap.Config.HARDWARE?
核心原因就四类:
- 这张图后面还要"处理",而 hardware Bitmap 不适合处理
- 这张图要走的软件绘制链路不支持
- 某些系统/设备上有兼容性或稳定性问题
- 一些中间态、动画帧、后处理流程必须用普通 Bitmap
1. 要做图片处理时,会禁用 hardware Bitmap
这是最常见的原因。
hardware Bitmap 的特点是:
- 不可变
- CPU 不方便直接访问像素
- 不能拿来随便做 Canvas(bitmap) 绘制
- 很多像素级操作不支持
所以只要发现这张图不是"纯展示",而是还要继续处理,就通常会禁用。
常见会触发禁用的操作
比如:
- 圆角
- 模糊
- 裁剪
- BitmapTransformation
- 颜色矩阵
- 像素读取
- Palette 取色
- OCR / ML / 二维码识别
- 自定义后处理器
- 多张图合成
例子
如果在 Glide 里写了:
Kotlin
Glide.with(imageView)
.asBitmap()
.load(url)
.transform(RoundedCorners(10))
.into(imageView)
这类 transform 往往就要求普通 Bitmap,因为它需要在 CPU/Canvas 上处理像素。
2. 软件绘制链路不支持 hardware Bitmap
这是另一个非常关键的原因。
如果某张图最终不是走"标准硬件加速 View 显示",而是要进入一些软件 Canvas 或特殊目标,hardware Bitmap 就容易出问题,甚至直接抛异常。
典型报错类似:
Software rendering doesn't support hardware bitmaps
常见不适合的场景
比如:
- Canvas(bitmap) 离屏绘制
- 软件渲染的 View
- setLayerType(View.LAYER_TYPE_SOFTWARE, ...)
- 某些通知 / RemoteViews / AppWidget
- 打印、截图、导出
- 某些自定义 View 内部会把 Drawable 画到 software canvas
- 需要把图片再转成普通 Bitmap 做二次绘制
所以图片库如果发现 target 或后续链路可能是软件渲染,就会保守地禁用 hardware。
3. 某些 Android 版本 / 机型上有兼容性问题
这也是 Glide/Coil很在意的一点:
性能优化不能换来线上 crash。
hardware Bitmap 从 Android 8.0(API 26)开始支持,但早期尤其是:
- Android 8.0 / 8.1
- 某些厂商 ROM
- 某些 GPU 驱动实现
存在一些兼容性坑。
常见问题包括
3.1 文件描述符(FD)占用问题
hardware Bitmap 底层通常关联 GraphicBuffer / HardwareBuffer 一类资源,可能会占用额外系统资源,包含文件描述符。
如果短时间内加载很多图,尤其在列表、大图流、复杂页面里,可能导致:
- FD 数量逼近上限
- 打开资源失败
- 渲染异常
- crash
三方库会根据系统版本、设备情况、FD 风险,决定是否允许 hardware config。
3.2 厂商实现 bug
有些设备上可能会出现:
- 绘制异常
- 共享元素过渡问题
- 某些场景下黑图
- 回收时机异常
- 特定系统 API 配合不稳定
所以库作者通常会维护一套"黑名单 / 兼容策略",发现不稳就禁用。
22. 动图、后处理、中间帧经常不能用 hardware Bitmap
如果是这些类型的资源,通常也更倾向禁用:
- GIF
- Animated WebP
- 动画帧序列
- 视频帧抽取后的继续处理
- 需要 postprocessor 的图片
原因很简单:
这些流程里,图片往往不是"解完就显示",而是:
- 要解多帧
- 要频繁变更
- 要合成
- 要后处理
- 要生成中间结果
而 hardware Bitmap 不适合当这种"处理中间态"。
23. 三个库分别为什么会这么做
Glide 为什么有时禁用
Glide 对 hardware Bitmap 的策略一直比较谨慎,原因主要有:
1 有 Transformation 时通常会禁用
因为 Glide 的很多变换本质上都需要普通 Bitmap。
比如:
- centerCrop
- circleCrop
- RoundedCorners
- 自定义 BitmapTransformation
这些都可能要求软件可处理的像素数据。
2 某些设备 / 系统版本有黑名单或保护逻辑
Glide 会考虑:
- Android 版本
- 厂商机型
- FD 使用风险
避免为了性能优化引入 crash。
3 某些目标对象不适合 hardware
如果最终拿到图的人不是单纯 ImageView 显示,而是还要继续处理,Glide 会倾向禁用。
可以主动关闭
Glide.with(context)
.load(url)
.disallowHardwareConfig()
.into(imageView)
24. Coil 为什么有时禁用
Coil 的设计也类似,核心原则是:
如果图片只是展示,可以用 hardware;
如果要处理,就回退软件 Bitmap。
1 有 Transformation 时通常会禁用
Coil 的 transformation 也需要对 Bitmap 做实际处理,所以一般不适合 hardware。
2 某些 Target / 使用场景不兼容
比如:
- 你要拿结果继续处理
- 自定义 Target 里要访问 Bitmap
- 目标链路不是单纯硬件加速绘制
Coil 通常也会选择禁用。
3 允许手动指定
ImageRequest.Builder(context)
.data(url)
.allowHardware(false)
.build()
如果后面要做调色、识别、处理,最好直接关掉。
有后处理器(Postprocessor)时
如果要做后处理,通常要用普通可处理 Bitmap 或 native buffer,而不是 hardware Bitmap。
动图 / 多帧资源
这类资源的内部管线更复杂,通常也不适合直接走 hardware Bitmap。
从根上说:图片库要的是"通用正确",不是"局部最快"
这是最本质的一点。
如果只看一个简单场景:
- 静态 JPEG
- 直接显示到 ImageView
- 不做任何处理
那 hardware Bitmap 很棒,确实能减少普通 Bitmap 的那种 CPU->GPU 上传路径。
但是图片库面对的真实业务是:
- 同一套 API 既要支持展示,也要支持变换
- 既要支持新系统,也要支持老设备
- 既要快,也不能崩
- 既要支持标准 View,也要支持各种奇怪 Target
所以它们的策略通常是:
只有在"确定安全且有收益"的场景下才启用 hardware Bitmap。
否则就回退到普通 Bitmap。
25. 可以把它理解成一个决策规则
大概就是这样:
会倾向开启 hardware Bitmap
- Android 8.0+
- 静态图片
- 只显示到硬件加速的 ImageView
- 不做任何 Bitmap 变换/像素处理
- 设备兼容性良好
会倾向禁用 hardware Bitmap
- 有 transform / postprocessor
- 需要读写像素
- 需要软件 Canvas
- 要调色、识别、导出、合成
- 动图/多帧
- 老系统或问题机型
- 资源/FD 风险较高
26. 建议
如果图片只是用来显示:
- 可以让库默认策略决定
- 一般它会在合适时启用 hardware
如果你的图片后面还要处理:
- Coil:allowHardware(false)
这是最稳的。
27. 简洁总结
Glide/Coil有时禁用 hardware Bitmap,不是因为它"不好",而是因为它**太偏"显示优化"**了。
而图片库的职责远比"显示"更广,所以一旦涉及:
- 图像处理
- 软件绘制
- 动画帧
- 兼容性
- 稳定性
它们就会主动退回普通 Bitmap。