Android RecyclerView图像类ViewHolder离屏缓存数量直接增加Java/View对象内存,间接增加Graphics/GL内存
2026/7/16 8:46:44
网站开发
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.1setItemViewCacheSize(40)这个设置的是 RecyclerView 内部的mCachedViews数量。它缓存的是刚离屏、但 RecyclerView 认为很快可能复用的 ViewHolder特点是1. itemView 还在 2. ViewHolder 还在 3. ImageView 还在 4. ImageView 当前 drawable/bitmap 很可能还在 5. 通常不会立刻触发 onViewRecycled() 6. 再次滑回来时可能不重新 bind所以在图片流场景里setItemViewCacheSize(40)比较容易导致离屏的 40 个 item 仍然持有图片资源这是最需要关注的。1.2recycledViewPool.setMaxRecycledViews(0, 40)这个设置的是回收池里某个viewType最多缓存多少个 ViewHolder。注意这里的0是 viewTypesetMaxRecycledViews(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 / dmabuf2.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.33. 四宫格图片流如何估算页面是四宫格4列 N行假设屏幕宽度是 1080px四列情况下单个 item 图片宽度大约1080 / 4 270px考虑间距后可能是250px ~ 270px如果图片是正方形缩略图可以按270 × 270估算。4. 单张图片内存估算4.1 ARGB_8888 / RGBA_8888常见图片格式4 bytes / pixel单张 270 x 270270 × 270 × 4 291,600 bytes ≈ 0.278 MiB4.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 ~ 25MB4.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_8888540 × 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 HeapitemView / ViewGroup / ImageView是Java HeapDrawable Java wrapper是Java Heap普通 Bitmap Java wrapper是少量Java Heap普通 Bitmap 像素是如果 ImageView 仍持有Native Heap / Bitmap软件 Bitmap 上传后的 texture可能Graphics / GL / EGL / dmabufHardware Bitmap / HardwareBuffer是较明显Graphics / dmabuf / Gfx dev / EGLRecyclerView 缓存容器是但很小Java Heap图片库 MemoryCache间接受影响Java / Native / Graphics取决于实现对于四宫格图片流大头通常不是 ViewHolder Java 对象而是Bitmap 像素内存 GPU texture HardwareBuffer / dmabuf7. 一个实用估算公式可以用这个公式粗算总增量 ≈ 缓存图片数量 × 单张图片实际解码宽 × 实际解码高 × bytesPerPixel × factor其中bytesPerPixel: ARGB_8888 / RGBA_8888 4 RGB_565 2 RGBA_F16 / HDR 8factor硬解码 Hardware Bitmap: factor ≈ 1.0 ~ 1.3 软件 Bitmap但未显示或 GPU texture 已释放: factor ≈ 1.0 软件 Bitmap已经显示且 texture 未释放: factor ≈ 1.5 ~ 2.0对于你这个四宫格 270x270、N40硬解码: 40 × 270 × 270 × 4 × 1.0~1.3 ≈ 11MB ~ 15MB 软件 Bitmap 已显示: 40 × 270 × 270 × 4 × 2 ≈ 22MB如果实际解码是 540x540硬解码: ≈ 44MB ~ 58MB 软件 Bitmap 已显示: ≈ 89MB8. 如何实际量化建议做 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 秒 采集 meminfo8.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/bitmap12. 和 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 策略粗略估算四宫格 270x270ARGB_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 验证。天天向上网站