Android学习总结之Glide自定义三级缓存(面试篇)
一、三级缓存核心原理与设计
问题 1:为什么需要三级缓存?各层缓存的核心作用是什么?
回答核心
- 内存缓存:毫秒级快速响应,存储近期浏览的图片(如滑动列表来回切换的图片),通过 LRU 算法自动清理冷数据,通常占内存 15%。
- 磁盘缓存:持久化存储常用图片,解决内存容量限制,支持离线访问(如商品详情页图片),按 URL 哈希值命名避免重复,容量 100MB 左右。
- 网络缓存:结合 HTTP 缓存头(如 Cache-Control),避免重复下载,降低流量和服务器压力,容量 50MB,优先存高频访问图片。
- 分层优势:形成 “速度优先→持久化存储→网络优化” 的三级防护,覆盖 90% 以上的图片加载场景,提升用户体验和系统稳定性。
话术示例:
三级缓存的设计是为了解决图片加载中的三个核心问题:
- 内存缓存(快):就像手机的 “最近使用列表”,存用户刚看过的图片(比如滑动列表时来回切换的图片)。用 LRU 算法自动清理太久没看的图片,比如 8GB 内存的手机分 1.2GB 给它,还会把大图片压缩到屏幕大小,确保滑动时瞬间加载,不卡顿。
- 磁盘缓存(稳):相当于 “本地相册”,存常用但暂时不在内存里的图片(比如用户常看的商品详情页图)。存 100MB 左右,用 URL 的哈希值命名避免重复,即使手机重启或没网,也能从这里找到图片。
- 网络缓存(省):类似 “路由器记忆”,避免重复下载。比如用 OkHttp 缓存 50MB,配合服务器的缓存策略(比如设置 1 天有效期),下次打开同一张图直接从路由器拿,省流量还减轻服务器压力。
三层配合,比如用户滑动列表时,先从内存秒开,滑走后存磁盘,下次没网也能看,联网时优先用网络缓存避免重复下载,覆盖 90% 的使用场景。
问题 2:LruCache 算法的核心数据结构和工作机制是什么?
回答核心:
- 数据结构:双向链表(维护访问顺序)+ 哈希表(快速查找)。
- 工作机制:
- 新元素加入链表头部,存在元素访问后移到头部;
- 缓存满时删除链表尾部元素(最久未使用);
- 通过
sizeOf()
方法计算元素大小,支持自定义内存占用(如 Bitmap 的字节数)。
- 大厂优化点:Glide 的 LruResourceCache 结合弱引用队列,感知 Bitmap 回收,避免内存泄漏,同时复用资源池对象减少创建开销。
话术示例:
LruCache 就像一个 “智能书架”,核心是按 “最近最少用” 原则管理图片:
- 每次取图片(访问),就把它放到书架最前面(双向链表头部),很久没取的书(最久未用)放在最后面(链表尾部);
- 书架满了(内存不够),直接扔掉最后一本书(删除尾部元素),保证书架始终不超载。
Glide 在这个基础上做了两个关键优化:
- 防内存泄漏:给图片加 “弱引用”,就像给书贴一个标签,书被卖掉(系统回收)时,标签自动从书架移除,避免书架上留空标签(无效引用);
- 资源复用:把用过的图片暂时存到 “回收箱”(资源池),下次需要相同尺寸的图片,直接从回收箱拿,不用重新买(创建 Bitmap),比如滑动列表时,同尺寸的图片反复用,内存占用能降 30% 以上。
二、常见问题解决方案(缓存穿透 / 雪崩 / OOM)
问题 1:缓存穿透的本质是什么?大厂如何解决高并发下的穿透问题?
回答核心:
- 本质:请求不存在的数据,导致每次都查数据库 / 网络,形成流量黑洞。
- 解决方案:
- 布隆过滤器:提前将所有合法 URL 存入布隆过滤器,请求时先过滤无效 URL,拒绝率达 99% 以上;
- 缓存空值:对不存在的 URL 缓存一个特殊值(如 NULL),设置短过期时间(5 分钟),防止恶意攻击;
- 占位图策略:Glide 中设置
error()
/fallback()
占位图,避免界面闪烁,同时记录无效请求日志。
问题 2:缓存雪崩的危害及多级预防策略?
回答核心:
- 危害:大量缓存同时失效,瞬间流量冲击数据库,可能导致服务雪崩。
- 预防策略:
- 错峰过期:给缓存时间添加随机偏移(如 24 小时 ±1 小时),避免集中失效;
- 多级缓存:内存 + 磁盘 + CDN 三层缓存,CDN 层抗住 80% 静态资源请求;
- 熔断降级:流量突增时,返回低清图或占位图,保证核心流程可用;
- 互斥锁:缓存失效时,仅允许一个线程重建缓存,其他线程阻塞等待,避免并发查库。
话术示例:
缓存穿透是 “无效请求攻击”,比如恶意用户大量请求不存在的图片 URL,导致每次都要查数据库,就像有人一直按门铃问 “10086 号房在吗”,但小区根本没这个房号。
大厂用三层防线解决:
- 门口装 “门禁”(布隆过滤器):提前把所有存在的房号(URL)录入门禁系统,访客(请求)先刷门禁,不存在的直接拦在门外,准确率 99% 以上,比如电商 APP 用布隆过滤器,每天能拦截 10 万 + 无效请求;
- 留 “空房记录”(空值缓存):对查过不存在的房号,记下来 “10086 号房不存在”,有效期 5 分钟,期间再有人问直接说 “不用查了”,防止短时间内重复攻击;
- 门口贴 “提示牌”(占位图):在 Glide 里设置默认图,比如请求失败时显示 “图片加载中” 的占位图,用户看不到空白,体验更好,同时后台记录这些无效请求,方便定位攻击源。
问题 3:如何从源头预防 OOM?Glide 中的关键配置有哪些?
回答核心:
- 内存优化三原则:
- 尺寸压缩:按 ImageView 尺寸加载(
override(width, height)
),避免加载超分辨率图片; - 格式转换:使用 RGB_565(比 ARGB_8888 节省 50% 内存)或 WebP 格式,Glide 中通过
format(DecodeFormat.PREFER_RGB_565)
配置; - 生命周期绑定:Glide 自动与 Activity/Fragment 绑定,界面不可见时清理资源,避免内存泄漏,同时可手动调用
clear(imageView)
释放。
- 尺寸压缩:按 ImageView 尺寸加载(
- 进阶策略:动态调整内存缓存大小(如低端机设为 10%,高端机 20%),结合
skipMemoryCache(true)
跳过非当前屏幕图片的内存存储。
话术示例:
OOM 就像 “书包塞满了大书”,图片太大或存太多就会撑爆内存。Glide 通过三个 “瘦身” 技巧预防:
- 按尺寸买书:比如手机屏幕是 1000px,就只加载 1000px 的图,不加载 2000px 的原图,用
override(1000, 1000)
强制压缩,内存占用直接减半; - 选轻便包装:用 RGB_565 格式代替默认的 ARGB_8888,前者每个像素占 2 字节,后者占 4 字节,同样一张图,内存占用少一半,配置代码:
Glide.with(context).load(url).format(DecodeFormat.PREFER_RGB_565);
- 定期断舍离:Glide 会自动跟着 Activity/Fragment 的生命周期清理内存,比如页面关掉时,把相关图片从内存删掉,也可以手动调用
clear(imageView)
,避免 “过期图片” 占空间。
比如一个短视频 APP,通过这三个技巧,内存峰值能从 500MB 降到 300MB 以下,OOM 崩溃率下降 70%。
话术示例:
缓存雪崩就像 “电梯超载”,比如双十一零点,大量商品图片的缓存同时过期,几十万人同时请求,数据库像电梯一样可能被挤瘫。
大厂在大促时会做三件事:
- 错峰下班:给每个缓存设置不同的过期时间,比如原定 24 点过期,让有的 23 点 50 分过期,有的 0 点 10 分过期,像员工分批次下班,避免电梯拥挤。代码上可以加随机值:
int expireTime = 24*60*60 + new Random().nextInt(3600); // 过期时间波动±1小时
- 多级防护:最外层用 CDN 缓存(比如阿里云 OSS),扛住 80% 的图片请求,中间层用本地磁盘缓存,最后才到数据库,就像 “保安 + 前台 + 门禁” 三层过滤;
- 降级处理:如果流量实在太大,暂时给用户看低清图或模糊图,比如把 10MB 的高清图换成 1MB 的低清图,保证页面能加载,同时对数据库访问加锁,同一时间只允许一个线程更新缓存,其他线程等待,避免所有人同时挤向数据库。
三、HTTP 缓存与网络优化
问题 1:Cache-Control 头中 max-age、no-cache、no-store 的区别和使用场景?
回答核心:
- max-age=3600:缓存有效期 1 小时,期间直接读本地缓存,适合不常更新的图片(如商品主图);
- no-cache:每次请求需服务器验证缓存有效性(发 304 请求),适合频繁更新但需浏览器缓存的图片(如活动海报);
- no-store:禁止任何形式的缓存,响应内容不落地,适用于敏感图片(如用户证件照)。
- 最佳实践:同时配置 ETag 和 Last-Modified,ETag 做精准校验(解决时间戳精度问题),Last-Modified 做快速判断,提升 304 命中率。
问题 2:客户端如何强制获取最新图片?服务器端如何配合?
回答核心:
- 客户端方案:
- URL 添加版本号或时间戳(如
image.jpg?v=2
),破坏缓存键一致性; - 设置请求头
Cache-Control: no-cache
,强制服务器验证; - 清除本地网络缓存(OkHttp 中通过
cache.remove(request)
)。
- URL 添加版本号或时间戳(如
- 服务器端配合:
- 返回正确的 Cache-Control 头(如更新时设置
max-age=0
); - 图片变更时更新 ETag 值,确保客户端能检测到变化。
- 返回正确的 Cache-Control 头(如更新时设置
四、性能监控与架构设计
问题 1:如何计算缓存命中率?大厂关注哪些核心指标?
回答核心:
- 计算公式:缓存命中率 =(内存命中数 + 磁盘命中数)÷ 总请求数 × 100%。
- 监控手段:
- Glide 开启 DEBUG 日志,筛选
Fetched from memory cache
和Fetched from disk cache
条目; - 自定义 ModelLoader 统计各层命中次数;
- 关注衍生指标:内存峰值(Android Profiler 监控 Heap Size)、加载耗时(System.currentTimeMillis () 打点)、FPS(确保滑动≥55fps)。
- Glide 开启 DEBUG 日志,筛选
- 优化目标:内存命中率≥30%,磁盘命中率≥50%,整体命中率≥80%。
问题 2:设计一个高并发图片缓存系统,需要规避哪些坑?
回答核心:
- 核心坑点与对策:
- 热点问题:高频图片集中失效,通过 “热点缓存 + 本地副本” 解决(如 Redis 热 key + 本地 Ehcache);
- 存储碎片化:URL 哈希冲突(概率极低),通过加盐哈希或 SHA-256 提升唯一性;
- 跨平台一致性:多端(Android/iOS/Web)缓存策略统一,如使用相同的 URL 参数规则和 Cache-Control 配置;
- 容量失控:磁盘缓存设置严格的 LRU 淘汰策略 + 过期时间(如 7 天未访问则删除),定期清理僵尸文件。