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

天天向上网站

相关推荐
众少成多积小致巨2 小时前
Android C++ 编码规范指南
android·c++·google
DavidSu4 小时前
Kotlin协程原理解析
android
lxysbly4 小时前
Android PS3模拟器下载推荐2026|安卓PS3模拟器哪个好?手机玩PS3游戏指南
android·游戏·智能手机
qq_246839754 小时前
记一次 Flutter 预热帧引起的初始化问题
android·flutter
阿pin4 小时前
Android随笔-要怎样与AI相处
android·人工智能·ai
Kapaseker4 小时前
Android CLI 完全指南
android·kotlin
心中有国也有家5 小时前
AtomGit Flutter 鸿蒙客户端:MoodStorage 的 CRUD 集成验证
android·服务器·flutter·华为·harmonyos
solo_995 小时前
Android window属性全解析
android
心中有国也有家5 小时前
AtomGit Flutter 鸿蒙客户端:错误处理与优雅降级策略
android·javascript·flutter·华为·harmonyos