Android RecyclerView图像类ViewHolder离屏缓存数量直接增加Java/View对象内存,间接增加Graphics/GL内存

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 验证。

天天向上网站

相关推荐
松仔log25 分钟前
Java中级——组合和继承
android·java·开发语言
我命由我123454 小时前
Jetpack Compose - MaterialExpressiveTheme 与 MaterialTheme、ColorScheme
android·java·开发语言·java-ee·kotlin·android jetpack·android runtime
lilian2335 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
2601_955759625 小时前
ClaudeAPI成本中心与业务标签设计指南
android·java·数据库
余衫马5 小时前
2026最新版!Android Studio 极速安装与配置指南
android·ide·android studio
AFinalStone5 小时前
Android 7系统异常问题排查(五)Framework层(下)—System Server崩溃
android·tombstone·系统异常
AFinalStone5 小时前
Android 7系统异常问题排查(八)系统追踪—Trace机制与性能诊断
android·系统异常
极客猴子9 小时前
Android录音转写频繁卡顿?多款APP长时间会议场景稳定性实测
android·人工智能·智能手机·飞书
Kapaseker9 小时前
Boolean 变量到底该怎么命名?
android·kotlin
AFinalStone9 小时前
Android 7系统异常问题排查(六)应用异常(上)—ANR机制全解
android·系统异常