Android RecyclerView图像类ViewHolder离屏缓存数量直接增加Java/View对象内存,间接增加Graphics/GL内存
RecyclerView 四宫格图片流,4列N行
item 中展示图片
部分图片还是硬解码图片 / Hardware Bitmap
setItemViewCacheSize(40)
recycledViewPool.setMaxRecycledViews(0, 40)
这种情况下,缓存数量从默认值提高到 40,可能不只是增加 Java 层 ViewHolder 内存,还让更多离屏图片继续被 ImageView/Drawable 持有,从而增加:
Java Heap
Native Heap / Bitmap native pixels
Graphics / GL / EGL / dmabuf / GraphicBuffer
如果图片是硬解码图片,内存压力会更偏向:
Graphics / dmabuf / GraphicBuffer / HardwareBuffer
也就是"图形图像段"的内存。
1. 这两行分别缓存什么?
mRecyclerView?.setItemViewCacheSize(40)
mRecyclerView?.recycledViewPool?.setMaxRecycledViews(0, 40)
1.1 setItemViewCacheSize(40)
这个设置的是 RecyclerView 内部的 mCachedViews 数量。
它缓存的是:
刚离屏、但 RecyclerView 认为很快可能复用的 ViewHolder
特点是:
1. itemView 还在
2. ViewHolder 还在
3. ImageView 还在
4. ImageView 当前 drawable/bitmap 很可能还在
5. 通常不会立刻触发 onViewRecycled()
6. 再次滑回来时可能不重新 bind
所以在图片流场景里,setItemViewCacheSize(40) 比较容易导致:
离屏的 40 个 item 仍然持有图片资源
这是最需要关注的。
1.2 recycledViewPool.setMaxRecycledViews(0, 40)
这个设置的是回收池里某个 viewType 最多缓存多少个 ViewHolder。
注意这里的 0 是 viewType:
setMaxRecycledViews(0, 40)
如果图片 item 的 viewType 不是 0,那这行对图片 item 不一定生效。
RecycledViewPool 的影响取决于有没有在回收时清理图片:
override fun onViewRecycled(holder: VH) {
super.onViewRecycled(holder)
holder.imageView.setImageDrawable(null)
}
或者用图片库:
Glide.with(holder.imageView).clear(holder.imageView)
如果清理了,那么 pool 主要增加的是 ViewHolder/View 树内存;
如果没清理,pool 里的 ViewHolder 也可能继续持有 Bitmap/HardwareBuffer。
2. 是否会扩大 GPU/图形内存?
结论分两种情况。
2.1 普通软件 Bitmap
如果图片是普通软件 Bitmap,例如:
Bitmap.Config.ARGB_8888
内存模型通常是:
ImageView
-> BitmapDrawable
-> Bitmap Java object
-> Native bitmap pixels
当这张图显示到屏幕后,RenderThread/HWUI 还可能把它上传成 GPU texture。
所以一张软件 Bitmap 可能有两份主要内存:
1. CPU/native 像素内存
2. GPU texture 图形内存
粗略估算:
总占用 ≈ 2 × width × height × bytesPerPixel
其中:
Native Bitmap pixels 可能体现在 Native Heap / Bitmap
GPU texture 可能体现在 Graphics / GL mtrack / EGL mtrack / Gfx dev / dmabuf
2.2 硬解码图片 / Hardware Bitmap
如果图片是硬解码图片,例如:
Bitmap.Config.HARDWARE
ImageDecoder.ALLOCATOR_HARDWARE
Glide Hardware Bitmap
它的像素通常不在普通 Java Heap,也不一定体现在传统 Native Bitmap pixels 里,而是更接近:
HardwareBuffer / GraphicBuffer / dmabuf
这类内存更可能统计到:
Graphics
GL mtrack
EGL mtrack
Gfx dev
dmabuf
所以对于硬解码图片:
RecyclerView 缓存更多 ViewHolder,如果这些 ViewHolder 里的 ImageView 仍然持有硬解码图片,就可能更直接地扩大图形图像段内存。
粗略模型:
硬解码图片占用 ≈ width × height × bytesPerPixel × 对齐系数
对齐系数可以粗略取:
1.0 ~ 1.3
3. 四宫格图片流如何估算?
页面是四宫格:
4列 N行
假设屏幕宽度是 1080px,四列情况下,单个 item 图片宽度大约:
1080 / 4 = 270px
考虑间距后可能是:
250px ~ 270px
如果图片是正方形缩略图,可以按:
270 × 270
估算。
4. 单张图片内存估算
4.1 ARGB_8888 / RGBA_8888
常见图片格式:
4 bytes / pixel
单张 270 x 270:
270 × 270 × 4 = 291,600 bytes
≈ 0.278 MiB
4.2 普通软件 Bitmap,显示后可能有两份
Native Bitmap pixels ≈ 0.278 MiB
GPU texture ≈ 0.278 MiB
合计 ≈ 0.556 MiB / 张
如果缓存 40 个:
40 × 0.556 MiB ≈ 22.2 MiB
所以对于四宫格 270x270 的软件 Bitmap,如果离屏缓存的 40 个 item 都还持有图片,并且 GPU texture 也未释放,粗略可能增加:
约 20MB ~ 25MB
4.3 硬解码图片 / Hardware Bitmap
硬解码图片大概:
270 × 270 × 4 ≈ 0.278 MiB / 张
缓存 40 个:
40 × 0.278 MiB ≈ 11.1 MiB
考虑 GraphicBuffer stride/对齐/额外开销:
约 11MB ~ 15MB
这部分更可能体现在:
Graphics / dmabuf / GL / EGL
如果部分图片是硬解码,部分是软件 Bitmap,那么实际值介于两者之间。
5. 如果实际解码尺寸大于显示尺寸,内存会迅速放大
这是图库里很常见的问题。
虽然四宫格 item 显示是:
270 × 270
但如果图片库实际解码出来的是:
540 × 540
那么单张 ARGB_8888:
540 × 540 × 4 = 1,166,400 bytes
≈ 1.11 MiB
硬解码 40 张:
40 × 1.11 MiB ≈ 44.4 MiB
软件 Bitmap 显示后可能:
Native + GPU ≈ 88.8 MiB
如果实际解码成:
720 × 720
单张:
720 × 720 × 4 = 2,073,600 bytes
≈ 1.98 MiB
硬解码 40 张:
约 79MB+
软件 Bitmap 显示后:
可能 150MB+
所以最关键不是单纯缓存数量,而是:
缓存数量 × 实际 decode 后图片尺寸
6. RecyclerView 缓存导致的内存由哪些部分组成?
可以拆成下面几类。
| 内存来源 | 是否受缓存数量影响 | 主要统计位置 |
|---|---|---|
| ViewHolder 对象 | 是 | Java Heap |
| itemView / ViewGroup / ImageView | 是 | Java Heap |
| Drawable Java wrapper | 是 | Java Heap |
| 普通 Bitmap Java wrapper | 是,少量 | Java Heap |
| 普通 Bitmap 像素 | 是,如果 ImageView 仍持有 | Native Heap / Bitmap |
| 软件 Bitmap 上传后的 texture | 可能 | Graphics / GL / EGL / dmabuf |
| Hardware Bitmap / HardwareBuffer | 是,较明显 | Graphics / dmabuf / Gfx dev / EGL |
| RecyclerView 缓存容器 | 是,但很小 | Java Heap |
| 图片库 MemoryCache | 间接受影响 | Java / Native / Graphics,取决于实现 |
对于四宫格图片流,大头通常不是 ViewHolder Java 对象,而是:
Bitmap 像素内存
GPU texture
HardwareBuffer / dmabuf
7. 一个实用估算公式
可以用这个公式粗算:
总增量 ≈ 缓存图片数量 × 单张图片实际解码宽 × 实际解码高 × bytesPerPixel × factor
其中:
bytesPerPixel:
ARGB_8888 / RGBA_8888 = 4
RGB_565 = 2
RGBA_F16 / HDR = 8
factor:
硬解码 Hardware Bitmap:
factor ≈ 1.0 ~ 1.3
软件 Bitmap,但未显示或 GPU texture 已释放:
factor ≈ 1.0
软件 Bitmap,已经显示且 texture 未释放:
factor ≈ 1.5 ~ 2.0
对于你这个四宫格 270x270、N=40:
硬解码:
40 × 270 × 270 × 4 × 1.0~1.3
≈ 11MB ~ 15MB
软件 Bitmap 已显示:
40 × 270 × 270 × 4 × 2
≈ 22MB
如果实际解码是 540x540:
硬解码:
≈ 44MB ~ 58MB
软件 Bitmap 已显示:
≈ 89MB
8. 如何实际量化?
建议做 A/B 对比,最靠谱。
8.1 改缓存值做对比
低缓存组:
mRecyclerView?.setItemViewCacheSize(0)
mRecyclerView?.recycledViewPool?.setMaxRecycledViews(0, 5)
或更实际一点:
mRecyclerView?.setItemViewCacheSize(4 * 2) // 四宫格两行,8个
mRecyclerView?.recycledViewPool?.setMaxRecycledViews(0, 20)
高缓存组:
mRecyclerView?.setItemViewCacheSize(40)
mRecyclerView?.recycledViewPool?.setMaxRecycledViews(0, 40)
固定测试路径:
进入页面
等待首屏加载完成
滑动 3~5 屏
停止 3 秒
采集 meminfo
8.2 看 dumpsys meminfo
执行:
adb shell dumpsys meminfo your.package.name
重点看:
Java Heap
Native Heap
Graphics
GL mtrack
EGL mtrack
Gfx dev
TOTAL PSS
如果缓存从 8 提到 40 后:
Java Heap 小幅上涨
Native Heap 上涨
Graphics / GL / EGL / dmabuf 明显上涨
说明缓存 ViewHolder 导致图片资源,尤其图形资源,被更多保留。
8.3 看 dmabuf
如果平台支持:
adb shell dumpsys dmabuf
或者:
adb shell cat /proc/meminfo
关注和 dmabuf/graphics 相关变化。
硬解码图片多的时候,dumpsys meminfo 的 Native Heap 未必特别明显,但:
Graphics
dmabuf
Gfx dev
EGL/GL mtrack
可能更明显。
8.4 打印每张图片实际 Bitmap 信息
在图片加载成功后,打印:
val drawable = imageView.drawable
val bitmap = (drawable as? BitmapDrawable)?.bitmap
Log.d(
TAG,
"bitmap info: " +
"w=${bitmap?.width}, " +
"h=${bitmap?.height}, " +
"config=${bitmap?.config}, " +
"byteCount=${bitmap?.byteCount}, " +
"allocation=${bitmap?.allocationByteCount}"
)
重点看:
bitmap.config == Bitmap.Config.HARDWARE
bitmap.width
bitmap.height
如果看到实际 Bitmap 是:
540x540
720x720
1080x1080
而不是接近 item 的 270x270,那缓存 40 个会非常占内存。
9. onViewRecycled() 不一定能释放 itemViewCache 里的图片
这一点非常重要。
setItemViewCacheSize(40) 缓存的 ViewHolder 可能停留在 RecyclerView 的 cached views 中,而不是进入 recycled pool。
这意味着:
onViewRecycled() 不一定会立刻调用
所以即使在 onViewRecycled() 里做了:
imageView.setImageDrawable(null)
也未必能释放 mCachedViews 中那 40 个 item 的图片。
因此:
setItemViewCacheSize(40)
在图片流里比:
recycledViewPool.setMaxRecycledViews(40)
更容易导致图片资源被保留。
10. 对四宫格图片流的建议值
对于四宫格图片流,尤其含硬解码图片:
setItemViewCacheSize(40)
建议从较保守的值开始:
val columnCount = 4
mRecyclerView?.setItemViewCacheSize(columnCount * 2) // 8
mRecyclerView?.recycledViewPool?.setMaxRecycledViews(0, columnCount * 5) // 20
或者:
mRecyclerView?.setItemViewCacheSize(columnCount * 3) // 12
mRecyclerView?.recycledViewPool?.setMaxRecycledViews(0, columnCount * 10) // 40
更推荐:
itemViewCacheSize 小一些
recycledViewPool 可以适当大一些
但 onViewRecycled 必须 clear 图片
因为:
itemViewCacheSize 保留的是"带内容的 ViewHolder"
recycledViewPool 更适合保留"清空后的空壳 ViewHolder"
11. 必须确认回收清理逻辑
建议 Adapter 里确保:
override fun onViewRecycled(holder: PhotoViewHolder) {
super.onViewRecycled(holder)
// 如果使用 Glide
Glide.with(holder.imageView).clear(holder.imageView)
// 或至少
holder.imageView.setImageDrawable(null)
}
如果是自定义图片加载框架,也要做:
取消未完成请求
清理 ImageView drawable
释放对 Bitmap/HardwareBitmap 的引用
如果有自定义 ImageView,要确认它真的清理了:
ImageView drawable
Bitmap 引用
图片加载任务
硬解码资源引用
tag 中的 media/bitmap
12. 和 RenderThread / gralloc 卡顿的关系
DrawFrames 100ms+
dequeueBuffer / allocateHelper 慢
Uninterruptible Sleep non-IO
如果四宫格图片流缓存了大量硬解码图片,会增加:
Graphics / dmabuf / GraphicBuffer 压力
这可能间接导致:
RenderThread 申请或等待 GraphicBuffer 更慢
gralloc 分配慢
GPU texture / fence 等待变长
dequeueBuffer 卡顿
所以这个缓存值不仅是内存问题,也可能是滑动/返回动画卡顿的诱因之一。
13. 结论
mRecyclerView?.setItemViewCacheSize(40)
mRecyclerView?.recycledViewPool?.setMaxRecycledViews(0, 40)
在四宫格图片流中,确实可能扩大 GPU/图形图像段内存占用。
具体表现取决于:
1. ImageView 是否仍持有 Bitmap/Drawable
2. 图片是否是 Bitmap.Config.HARDWARE
3. 实际解码尺寸是否接近 item 显示尺寸
4. onViewRecycled 是否清理图片
5. 图片库自身 memory cache / active resources 策略
粗略估算:
四宫格 270x270,ARGB_8888,缓存 40 个:
硬解码图片:
约 11MB ~ 15MB Graphics/dmabuf
软件 Bitmap 且已显示:
约 20MB ~ 25MB,分布在 Native Heap + Graphics/GL/EGL
如果实际 decode 是 540x540:
硬解码:
约 44MB ~ 58MB Graphics/dmabuf
软件 Bitmap 且已显示:
可能接近 90MB
建议:
1. 不要把 itemViewCacheSize 直接设到 40,四宫格建议先试 8~12
2. recycledViewPool 可以设大一些,但必须 onViewRecycled clear 图片
3. 打印 Bitmap 实际 width/height/config
4. 用 dumpsys meminfo 对比 Graphics / Native Heap / GL / EGL
5. 如果硬解码图片较多,重点观察 dmabuf / Graphics 增量
一句话总结:
setItemViewCacheSize(40)在四宫格图片流,特别是含硬解码图片时,可能让 40 个离屏 item 继续持有 HardwareBitmap/GraphicBuffer,从而明显增加 Graphics/dmabuf 这类图形图像内存;实际增量可以按"缓存数量 × 实际解码宽高 × bpp × factor"估算,并通过dumpsys meminfo的 Graphics/GL/EGL/Native Heap 做 A/B 验证。