什么是 LruCache?如何在自定义图片加载器中实现一个高效的内存缓存?
解读
国内面试中,这道题既考察候选人对“内存缓存”这一高频优化点的理解深度,也暗含对“图片加载闭环”——下载→解码→缓存→复用→回收——的整体把控。面试官希望听到:
- 对 LruCache 数据结构的精确描述(LinkedHashMap 双向链表 + 访问顺序排序);
- 容量设定策略(可用内存比例、屏幕像素密度、业务场景);
- 与 Bitmap 强引用、软引用、弱引用的取舍;
- 线程安全与并发访问;
- 与磁盘缓存、复用池(BitmapPool)的协同;
- 版本差异(API 12 之前无 LruCache 时如何手写,Android 4.4 前后 Bitmap 内存模型差异);
- 线上灰度指标(缓存命中率、GC 次数、PSS 增长曲线)。
回答时切忌只背“最近最少使用”六个字,而要给出“可落地的数值 + 代码骨架 + 性能验证”。
知识点
- LruCache 源码级原理:LinkedHashMap 三参构造 accessOrder=true,每次 get/put 把节点移至队尾,trimToSize 从队头循环移除。
- Bitmap 内存占用计算:width * height * 每像素字节数(ARGB_8888 为 4 字节);在 Android 7.0 之前 Bitmap 像素存 native 层,7.0 之后移回 Java 堆,影响 GC 策略。
- 缓存容量设定:国内主流方案为“运行时最大内存的 1/8~1/6”,再按屏幕分辨率做系数修正,例如 1080P 手机取 1/6,2K 屏取 1/8,防止高分辨率图片瞬间打满。
- 并发安全:LruCache 自带 synchronized,但自定义加载器内部仍需把“内存缓存命中→UI 线程回调”与“磁盘/网络加载→线程池解码”两条路径拆成独立锁域,避免 UI 阻塞。
- Bitmap 复用池:inBitmap 参数要求新 Bitmap 的内存 <= 旧 Bitmap 的内存,且 inSampleSize=1 时必须尺寸完全一致;国内厂商 ROM 对 inBitmap 校验策略略有差异,需灰度兼容。
- 缓存命中率监控:利用美团 Raptor、腾讯 Matrix 或字节 ByteHook 插桩,统计 LruCache.hitCount / missCount,线上低于 85% 即触发告警。
- 国内合规:若图片含用户头像,需遵循《个人信息保护法》加密缓存,禁用全局可读目录;若集成第三方 SDK,需在隐私清单中声明“SDCard 缓存”用途。
答案
-
LruCache 定义
Android 提供的泛型哈希链表缓存,内部用 LinkedHashMap 按访问顺序排序,当元素超过 maxSize 时,从队头移除“最近最少使用”项,时间复杂度 O(1)。 -
容量计算与初始化
val maxMemory = Runtime.getRuntime().maxMemory() val cacheSize = (maxMemory / 1024 / 6).toInt() // 单位 KB,约 1/6 val memoryCache = object : LruCache<String, Bitmap>(cacheSize) { override fun sizeOf(key: String, bitmap: Bitmap): Int { return bitmap.byteCount / 1024 // KB } } -
线程安全封装
对内存缓存做一层代理,所有读写走主锁,但把“解码”任务放到线程池,解码完成后再一次性提交到主线程,避免持有锁做耗时 IO。 -
与复用池协同
解码前从 BitmapPool 查找可复用对象,设置 Options.inBitmap;若复用失败则回退到普通解码。解码成功后将旧 Bitmap 回收到池中。 -
生命周期对齐
在 Activity.onLowMemory() 与 ComponentCallbacks2.onTrimMemory() 中分别调用 memoryCache.evictAll() 与 memoryCache.trimToSize(newSize),防止后台被系统杀死。 -
命中率验证
线下用 Systrace + Perfetto 观察:连续滑动 RecyclerView 200 张图片,目标帧率 ≥ 55 fps,缓存命中率 ≥ 90%,PSS 增长 ≤ 15 MB。
拓展思考
- 分层缓存架构:把“内存缓存→磁盘缓存→网络”做成责任链,国内弱网场景下磁盘缓存 TTL 可拉长至 7 天,但需在 HTTP 响应头加入 Cache-Control: max-age=604800,并兼容 CDN 劫持导致的 404 替换图。
- 大图片分片缓存:针对电商长图,先按 256 px 高切片,再对每片做 LruCache 缓存,滑动时按需解码,降低峰值内存。
- 零拷贝渲染:Android 10 以上引入 ImageDecoder,可把解码后像素直接映射到 HardwareBuffer,结合 SurfaceFlinger 的 GPU 纹理缓存,实现“解码→渲染”零次 memcpy,适合 4K 视频封面。
- 隐私沙盒适配:Android 13 细化 READ_MEDIA_IMAGES 权限,自定义加载器需把磁盘缓存目录迁移到 app-specific 路径,避免触发新权限弹窗;同时把缓存索引加密存储在 JetSec 提供的 EncryptedFile,防止 root 设备被拷走。
- 动态调参:利用 MVI 架构把“缓存命中率、GC 次数、电量消耗”作为状态流,后台下发实验参数,实时调整 cacheSize 与池大小,实现“千人千面”的内存策略。