Glide三级缓存
面试官
"我看你简历里提到熟悉 Glide,能聊聊它的缓存机制吗?比如加载图片的时候,Glide 是怎么决定从内存还是磁盘读取的?"
你
哦,Glide 的缓存机制是吧?嗯,这个我之前在做项目的时候还特意关注过,因为它对应用的流畅度和流量消耗影响还挺大的。我理解的是,Glide 在加载图片的时候,它会像剥洋葱一样,一层一层地去找缓存,目标是尽可能快地把图片显示出来,并且尽量少地去请求网络。
它首先会去内存里找。 Glide 的内存缓存我记得好像还有细分,比如有个叫"Active Resources "的,就是指那些图片当前正在界面上显示着呢,这种它会优先用,因为这些图片肯定是最活跃的。然后还有一个叫"LruResourceCache"的,就是基于 LRU 算法(Least Recently Used,最近最少使用)的内存缓存,存放那些最近用过但可能已经不在屏幕上显示的图片。如果图片在这两块内存缓存中的任何一个找到了,并且尺寸也合适,那 Glide 就直接从内存加载,这是最快的,几乎是瞬时的。
如果在内存里没找到,Glide 就会去看磁盘缓存。 这个磁盘缓存就不是直接把原始图片存起来那么简单了。Glide 会根据我们加载图片时设置的尺寸、做过的变换(比如裁剪、圆角什么的),把处理过的图片缓存到磁盘上。这样下次再请求同样来源、同样尺寸和变换的图片时,它就能直接从磁盘读出来,解码一下就能用了,比重新下载快多了,也省流量。
至于怎么决定从内存还是磁盘读取 ,其实就是上面说的这个顺序:先内存,后磁盘。
- 它会先生成一个基于图片URL、尺寸、变换等信息组合起来的唯一 key。
- 拿着这个 key 先去内存缓存(Active Resources 和 LruResourceCache)里找。
- 如果内存里有,太好了,直接用。
- 如果内存里没有,它就会再用这个 key(或者根据不同的磁盘缓存策略生成对应的key)去磁盘缓存里找。Glide 有好几种磁盘缓存策略,比如
DiskCacheStrategy.AUTOMATIC
、DiskCacheStrategy.RESOURCE
(只缓存处理后的图片)、DiskCacheStrategy.DATA
(只缓存原始数据) 等等。默认的AUTOMATIC
会根据图片来源智能判断,比如远程图片它可能既缓存原始数据也缓存处理后的结果。 - 如果磁盘里找到了对应缓存,就加载、解码、然后显示,并且通常还会把这张图片再放到内存缓存里一份,方便下次更快访问。
要是内存和磁盘缓存都没找到,那没办法了,Glide 才会去真正的图片来源加载,比如从网络下载,或者从手机本地文件读取。下载完成后,它又会根据我们设置的磁盘缓存策略把图片存到磁盘,并且也会放到内存缓存。
所以,整个流程就是:内存 -> 磁盘 -> 源(网络/本地)。它总是优先尝试从最快的地方获取图片。我感觉 Glide 在这方面做得挺智能的,帮我们开发者省了不少事儿。
(举例子加强理解)
"举个例子吧,就像刷朋友圈------刚看过的图片再滑回来,秒加载,因为内存缓存还在;但如果过了半小时再打开,可能就要从磁盘缓存加载了,这时候稍微慢一点,但不用重新下载。如果磁盘都没有,才会去网络请求,整个过程对用户来说基本是无感的。"
面试官(追问)
"听起来你对缓存策略有研究,那如果遇到低端机频繁 OOM,或者高端机内存浪费的问题,你会怎么优化?"
你(结合场景思考)
这个问题我之前在项目里遇到过。比如低端机内存小,Glide 默认的内存缓存是固定比例(比如总内存的 1/8)在实际开发中肯定会遇到。低端机怕 OOM,高端机又怕白白浪费了那么多内存,确实得好好平衡一下。
如果我遇到低端机频繁 OOM 的情况,我会琢磨这么几个方向去优化:
首先,我会想到减小 Glide 的内存缓存大小 。Glide 默认会根据设备的内存给一个建议值,但对于特别低端的机器,这个值可能还是太大了。我记得 Glide 好像是允许我们通过自定义 AppGlideModule
来设置一个更小的 MemoryCache
大小的,比如用 MemorySizeCalculator
来算一个更保守的值,或者干脆自己指定一个固定的大小。
然后,图片的解码格式 也是一个可以下手的地方。默认情况下,Glide 可能会用 ARGB_8888
这种高质量的格式,每个像素占4个字节。但如果有些图片,比如背景图或者一些不太需要透明通道的缩略图,是不是可以考虑降级到 RGB_565
?这样每个像素只占2个字节,内存占用直接减半,虽然会损失一点点图片质量,但在低端机上,保证流畅和不崩溃可能更重要。这个也是可以在 AppGlideModule
里配置全局的 DecodeFormat
。
再有就是,确保加载的图片尺寸和 ImageView 的尺寸匹配 。Glide 本身在这方面已经做得很好了,它会根据 ImageView 的大小来加载合适尺寸的图片,避免把一张超大的图加载到内存里然后缩小显示,那样特别浪费内存。但我们自己也得注意,比如在代码里动态加载图片到没有固定大小的 Target 时,最好用 override()
方法指定一个明确的尺寸。
我还想到,可以响应系统的内存回收回调 ,比如在 onLowMemory()
或者 onTrimMemory()
这些方法里,主动调用 Glide.get(context).clearMemory()
来清理掉 Glide 的内存缓存,给系统腾出一些空间。当然,磁盘缓存可以保留,下次还能用。
对于高端机内存浪费的问题,感觉思路会有点不一样:
高端机内存确实多,但也不能无限制地让 Glide 用内存。虽然多用点内存缓存能提高命中率,加载图片更快,但如果占太多,其他应用或者系统本身可能会受影响。
所以,即便是高端机,Glide 默认的内存缓存策略通常已经比较合理了。如果发现确实"浪费"了,比如内存占用很高但缓存命中率提升不明显,或者通过 Profiler 看到 Glide 占用了不必要的大块内存,可能还是得看看是不是有些地方用得不太对。
比如,是不是有些一次性使用或者非常大的图片也被长时间放在内存缓存里了 ?对于这种,我们可以在加载的时候用 skipMemoryCache(true)
让它不进入内存缓存,用完就释放。
还有就是,高端机可以适当利用它的性能和内存优势,比如磁盘缓存策略可以更积极一些,缓存更多尺寸或者类型的图片,这样从磁盘恢复的时候也很快。但内存缓存还是要在一个合理的范围内。
总的来说,我觉得关键还是"因地制宜"和"按需分配":
- 监控和分析是前提:我会先用 Android Studio 的 Profiler 工具看看内存具体是怎么用的,Glide 占了多少,哪些图片占得多,缓存命中率怎么样。
- 调整缓存大小和策略 :根据分析结果,针对性地调整内存缓存大小、磁盘缓存策略(比如
DiskCacheStrategy.AUTOMATIC
,RESOURCE
,DATA
,NONE
等)和 Bitmap 的解码格式。 - 合理使用 Glide 的 API :比如上面提到的
override()
,skipMemoryCache()
, 以及请求优先级Priority
等。 - 生命周期管理 :确保 Glide 的请求和组件的生命周期绑定妥当,避免内存泄漏。Glide 本身在这方面做得很好,比如
with(fragment)
或者with(activity)
。
(补充实战细节)
"另外,高端机的磁盘缓存也可以优化。比如把用户头像这种小图和壁纸大图分开存,就像手机里的'照片'和'文档'分文件夹管理。头像这种高频访问的放 SSD 分区,大图放 HDD 分区,这样磁盘 I/O 效率能提升不少。"
面试官(抛实际场景)
"假设现在有个列表页,用户快速滑动时,图片加载卡顿甚至白屏,你怎么解决?"
你
首先,我会重点看看 Glide 这边能做些什么优化:
- 预加载和请求优先级调整 :Glide 有个叫
preload()
的功能,可以在用户滑动到某个位置之前,提前把一些图片加载到缓存里。对于列表,尤其是知道滑动方向的话,这招可能挺管用。另外,我还会考虑在快速滑动(fling 状态)的时候,是不是可以暂停新的图片加载请求 ,等列表滑动慢下来或者停住的时候再恢复加载。我记得 Glide 好像有和RecyclerView
滚动状态结合的机制,或者我们可以自己监听RecyclerView
的OnScrollListener
,在SCROLL_STATE_FLING
的时候让 Glide 暂停请求,在SCROLL_STATE_IDLE
或SCROLL_STATE_TOUCH_SCROLL
的时候恢复。这样可以避免在快速滑动时,大量不可见的图片请求抢占资源,导致当前屏幕上该显示的图片加载不出来。
Kotlin
// 在你的 Activity 或者 Fragment 中设置 RecyclerView
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val context = recyclerView.context
if (!Glide.with(context).isPaused) { // 先判断一下当前状态,避免重复操作
when (newState) {
RecyclerView.SCROLL_STATE_FLING -> {
// 用户手指快速一滑,列表正在飞速滚动
Log.d("GlideScroll", "Flinging, pausing requests")
Glide.with(context).pauseRequests()
}
RecyclerView.SCROLL_STATE_IDLE -> {
// 列表停止滚动了
Log.d("GlideScroll", "Idle, resuming requests")
Glide.with(context).resumeRequests()
}
RecyclerView.SCROLL_STATE_DRAGGING -> {
// 用户手指还在屏幕上拖动列表
Log.d("GlideScroll", "Dragging, resuming requests")
Glide.with(context).resumeRequests()
}
}
}
}
})
- 缩略图和占位图是必须的 :一个好的占位图 (
placeholder()
) 能让用户在图片加载出来之前不至于看到一片空白,体验会好很多。更进一步,如果原图比较大,可以考虑先加载一个低分辨率的缩略图 (thumbnail()
),这个缩略图可能来自服务器的另一个小尺寸 URL,或者 Glide 也可以用一个比例因子(比如thumbnail(0.1f)
)来先加载一个小版本的图片,等原图加载好了再平滑过渡过去。这样至少能快速展示点东西,避免白屏。】
Kotlin
// 在 RecyclerView.Adapter 的 onBindViewHolder 方法里
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val imageUrl = imageUrls[position] // 假设这是图片URL列表
val context = holder.imageView.context
Glide.with(context)
.load(imageUrl)
.placeholder(R.drawable.placeholder_image) // 加载中的占位图
.error(R.drawable.error_image) // 加载失败时显示的图
.thumbnail(0.1f) // 先加载一个原图10%大小的缩略图,模糊但快速
// 或者用一个更小的独立URL作为缩略图:
// .thumbnail(Glide.with(context).load(thumbnailUrl).override(50, 50))
.into(holder.imageView)
}
- 图片尺寸要严格控制 :这点之前也提过,确保我们请求的图片尺寸和最终显示在
ImageView
上的尺寸是匹配的。如果列表项的ImageView
尺寸是固定的,比如说 100dp x 100dp,那 Glide 加载的时候就应该只加载这么大的图片到内存,而不是加载一张超大图再缩小,那样既慢又费内存,快速滑动时更容易卡顿。用override(width, height)
或者确保ImageView
的layout_width
和layout_height
是固定值能帮助 Glide 做到这一点。
Kotlin
// 还是在 onBindViewHolder 里
Glide.with(context)
.load(imageUrl)
.override(150, 150) // 指定加载的图片解码到内存中的尺寸
.centerCrop() // 或者 fitCenter(),根据需求来
.placeholder(R.drawable.placeholder_image)
.into(holder.imageView)
- 缓存策略检查:确认内存缓存和磁盘缓存都开启了并且配置得当。如果图片之前加载过,快速滑回去的时候应该能从内存或者磁盘缓存里秒出,这样就不会卡顿了。如果发现滑过的图片再次出现时还是重新加载,那就要检查缓存配置,或者是不是 URL 每次都变了导致缓存的 key 不一样。
Kotlin
// 1. 实现 PreloadModelProvider
class MyPreloadModelProvider(
private val context: Context,
private val imageUrls: List<String>,
private val preloadImageWidth: Int,
private val preloadImageHeight: Int
) : ListPreloader.PreloadModelProvider<String> {
override fun getPreloadRequestBuilder(item: String): RequestBuilder<*> {
// item 就是图片 URL
return Glide.with(context)
.load(item)
.override(preloadImageWidth, preloadImageHeight)
// 这里通常只下载到磁盘缓存,不解码显示
// .diskCacheStrategy(DiskCacheStrategy.DATA) // 或 .downloadOnly() in some Glide versions
}
override fun getPreloadItems(position: Int): List<String> {
// 返回接下来要预加载的图片URL,通常是当前位置后面的几个
return if (position + 1 < imageUrls.size) {
Collections.singletonList(imageUrls[position + 1])
} else {
Collections.emptyList()
}
// 实际项目中可能会预加载更多,比如后面3-5个
}
}
// 2. 在设置 RecyclerView 的地方使用
// val preloadSizeProvider = FixedPreloadSizeProvider<String>(imageWidth, imageHeight)
// val modelProvider = MyPreloadModelProvider(this, yourImageUrls, imageWidth, imageHeight)
// val preloader = RecyclerViewPreloader(
// Glide.with(this),
// modelProvider,
// preloadSizeProvider,
// 10 // maxPreload,一次最多预加载多少个
// )
// recyclerView.addOnScrollListener(preloader)
其次,除了 Glide 本身,RecyclerView 的使用方式也很关键:
- ViewHolder 的复用 :这个是 RecyclerView 的基础,确保 ViewHolder 被正确复用,
onBindViewHolder
方法尽可能轻量。不要在这里面做耗时操作,图片加载交给 Glide 异步去做。 setHasFixedSize(true)
:如果列表项的高度是固定的,调用recyclerView.setHasFixedSize(true)
可以让 RecyclerView 做一些内部优化,对性能有好处。- 减少不必要的刷新 :如果列表数据更新了,尽量用
DiffUtil
配合notifyItemChanged()
,notifyItemInserted()
等局部刷新方法,而不是粗暴地notifyDataSetChanged()
。这样可以避免整个列表重新绑定,减少不必要的图片加载。
如果上面这些常规操作都检查过了,问题还在,我可能会这样做:
- 用 Profiler 分析 :打开 Android Studio 的 Profiler,看看在快速滑动的时候,CPU 是不是被打满了,
onBindViewHolder
是不是执行时间过长,内存有没有异常抖动或者频繁 GC。这能帮我定位到具体的瓶颈。 - 检查是不是有其他耗时操作 :有时候卡顿不完全是图片加载的锅,可能是在
onBindViewHolder
里还有其他偷偷摸摸的计算、IO 操作,或者复杂的视图绘制。 - 考虑图片的数量和复杂度:如果一个列表项里图片特别多,或者图片本身需要很多处理(比如复杂的圆角、滤镜),也可能会导致性能问题。
"就像食堂打饭,人挤的时候别纠结选菜,先拿个包子垫垫肚子,等人少了再慢慢挑。"
面试官(考察原理)
"你提到 LruCache,它底层是怎么实现的?为什么用 LinkedHashMap?"
你(深入浅出)
"LruCache 的核心其实是'最近最少使用'的淘汰机制。LinkedHashMap 有个特性,如果初始化时传了 accessOrder=true
,每次调用 get()
方法,这个元素就会被移到链表头部,而淘汰的时候直接从尾部删除。这就像整理书架------最近看过的书放最前面,很久没看的书放最后面,书架满了就先扔最后面的。"
(关联实际优化)
"不过实际项目中,直接用它可能会有性能问题。比如大图频繁进出会导致链表频繁调整,所以我们项目里对大图做了'权重',让它们更快被淘汰,避免拖累整个缓存效率。"
面试官(压力题)
"如果现在让你改造 Glide 的磁盘缓存,24 小时内上线,你会优先改什么?"
你(冷静分析)
"首先我会抓取线上日志,看当前磁盘缓存的命中率和热点数据类型。如果发现用户头像这种小图和大图混存导致频繁淘汰,优先做冷热分离------把高频小图放在独立的小缓存区,大图单独分区,这样命中率能立竿见影提升。其次加个'预加载'策略,比如在 Wi-Fi 环境下提前缓存第二天要用的素材,用户打开时直接命中。"
(留有余地)
"当然,如果时间紧迫,可能先用 Glide 的 DiskCacheStrategy
调整缓存策略,比如只缓存原始数据(DATA),牺牲一点 CPU 换磁盘空间,同时监控 OOM 率,快速验证效果。"
面试官(收尾开放题)
"最后,如果让你设计一个全新的图片加载框架,你会注意哪些点?"
你(展现格局)
"首先会考虑'分层设计',比如把网络、解码、缓存拆成独立模块,方便替换底层库。比如网络层用 OkHttp 还是 Cronet,解码用 Skia 还是硬件加速,可以灵活配置。其次是'场景化适配',比如针对电商 App 的大图详情页和聊天 App 的小头像,提供不同的缓存策略和解码参数。最后是'可观测性',比如内置 Metrics 模块,实时监控内存、磁盘、流量的消耗,甚至预测 OOM 风险,主动降级。"
(用比喻收尾)
"就像造一辆车------发动机(性能)、后备箱(缓存)、仪表盘(监控)都得配合好,不同路况(场景)还能自动切换模式。"
面试官: 我看你简历提到熟悉Glide的三级缓存机制,能简单讲讲为什么需要三级缓存吗?
候选人: 好的。三级缓存的设计主要是为了平衡加载速度和资源消耗。比如用户滑动商品列表时,内存缓存能快速显示刚看过的图片,因为内存读取最快,这里用LruCache算法自动清理不常用的图片,一般占手机内存的15%。比如8GB的手机大约有1.2GB缓存,而且Glide很聪明,会自动压缩图片尺寸适配屏幕,减少内存占用。
如果图片不在内存里,比如用户昨天浏览过的商品,这时磁盘缓存就派上用场了。Glide用DiskLruCache存到手机硬盘,默认100MB,按URL哈希值命名避免重复保存。最后才是网络缓存,比如同一张图片被多个用户请求,通过OkHttp配合HTTP缓存头,可以复用基站或CDN的缓存,减少重复下载,像设置Cache-Control控制有效期,或者在URL里加版本号强制更新。
面试官: 如果让你设计一个自定义的图片缓存框架,你会怎么考虑?
候选人: 我之前尝试过仿照Glide的思路设计一个轻量级框架。首先是内存缓存,用Android自带的LruCache,根据手机内存动态分配缓存大小,比如最大内存的1/8。存图片时会计算Bitmap的内存占用,避免OOM。
然后是磁盘缓存,用Jake Wharton的DiskLruCache库,把图片存到应用缓存目录,文件名用URL的MD5哈希值,避免特殊字符问题。比如用户第一次加载网络图片后,会同时存到内存和磁盘,下次打开优先从内存拿,没有再查磁盘,最后才走网络。
多级缓存的关键是做好优先级和同步。比如网络请求到图片后,要同时更新内存和磁盘,下次就能快速命中。另外要考虑线程安全,比如磁盘读写要用异步任务,避免卡主线程。
面试官: 实际项目中遇到过缓存穿透或OOM的问题吗?怎么解决的?
候选人: 比如有一次测试反馈页面加载空白图,排查发现是图片URL错误导致频繁请求不存在的资源,这就是缓存穿透。后来在Glide里加了占位图策略,用error()
和fallback()
设置默认图,同时和后台约定对非法请求直接返回占位图URL,避免频繁回源查询。
OOM的问题在列表页比较常见,尤其是高清图。我们的优化方案分几步:一是用Glide的override()
限制图片尺寸,比如缩放到ImageView的2倍大小;二是改用RGB_565格式,虽然颜色质量差一点,但内存减半;三是监控Bitmap内存,通过Android Profiler发现某些图片解码参数有问题,比如BitmapFactory.Options的inPreferredConfig
没正确设置。
面试官: 你们是怎么评估缓存效果的呢?比如命中率或性能提升?
候选人: 主要通过几个指标:
- 冷启动时间:用Android Profiler的CPU跟踪,对比开启缓存前后的首屏加载时间,比如从1.2秒降到400毫秒;
- 内存峰值:在疯狂滑动列表时,用Memory Profiler观察堆内存,如果缓存合理,内存会稳定在某个区间,不会持续上涨;
- 缓存命中率 :Glide开启DEBUG日志后,统计日志里
MemoryCache
和DiskCache
的命中次数,比如总请求100次,内存命中30次,磁盘命中50次,剩下的20次走网络,命中率就是80%; - 帧率:用开发者选项里的GPU渲染跟踪,确保滑动列表时帧率在55 FPS以上,避免缓存解码耗时导致卡顿。
面试官: 如果现在要你优化一个现有缓存的APP,你会从哪入手?
候选人: 我可能会分四步走:
- 现状分析:先用工具抓取性能数据,比如用Stetho检查网络请求是否重复,用LeakCanary查内存泄漏,确定瓶颈在哪里;
- 策略调整:比如发现磁盘缓存太小,导致频繁请求网络,就调大DiskLruCache到200MB;或者发现内存缓存命中率低,可能LruCache的size计算不合理;
- 代码级优化 :比如用ARTHook检测Bitmap分配,发现某些图片没复用,改用
BitmapPool
;或者网络层用OkHttp的拦截器添加缓存头;
Glide网络缓存专题
面试官: 你提到Glide用网络缓存避免重复下载,能具体说说它是怎么实现的吗?
候选人: 好的,Glide的网络缓存其实是通过集成OkHttp实现的。比如我们加载一个图片URL时,OkHttp会先检查HTTP响应头里的Cache-Control
或者Expires
字段,确定这张图片能不能缓存、能存多久。举个例子,如果服务器返回Cache-Control: max-age=86400
,Glide就会把这张图在本地存24小时,这段时间内再请求同一张图,哪怕应用重启了,只要没过期,就直接用本地缓存,不会再走网络。
不过实际项目中可能会遇到服务器不给缓存头的情况,这时候我们可以在OkHttp里强制加缓存策略。比如在拦截器里统一设置缓存时间:
Kotlin
val okHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val response = chain.proceed(chain.request())
response.newBuilder()
.header("Cache-Control", "public, max-age=86400") // 强制缓存24小时
.build()
}
.cache(Cache(context.cacheDir, 50 * 1024 * 1024)) // 50MB缓存空间
.build()
这样即使服务器没配置,客户端也能主动控制缓存逻辑,特别适合商品详情页这种图片更新不频繁的场景。
面试官: 如果遇到图片更新但缓存未失效的情况,比如用户头像换了,Glide怎么处理?
候选人: 这个问题我们确实遇到过。Glide的处理方案很巧妙------在URL里加版本号。比如用户上传新头像后,服务端返回的URL会变成https://xxx.com/avatar.jpg?v=2
,和旧URLavatar.jpg?v=1
不同,这样Glide就会认为这是两张不同的图片,自动重新下载。
如果服务端不支持改URL,还可以用时间戳当参数,比如avatar.jpg?timestamp=1620000000
,但这种方案要谨慎使用,避免缓存完全失效。另外,Glide的signature()
方法也能强制刷新缓存。比如检测到用户信息变更时,生成一个新的签名:
Kotlin
Glide.with(context)
.load(url)
.signature(new ObjectKey(System.currentTimeMillis())) // 用时间戳作为签名
.into(imageView);
这样即使URL不变,Glide也会认为图片需要更新,优先走网络请求。
面试官: 你们是怎么监控网络缓存命中率的?有没有实际优化案例?
候选人: 我们主要通过两种方式监控:
- OkHttp日志拦截器 :在Debug包中加一个日志拦截器,统计
Cache Hit
和Cache Miss
的次数。比如看到日志输出Response from: cached response
就表示命中缓存。 - Charles抓包:在测试阶段用Charles抓包,对比相同URL的请求次数。比如商品列表图片滑动三次,如果网络请求只出现一次,说明缓存生效了。
实际优化案例:之前商品详情页的图片加载平均耗时1.2秒,分析发现90%的图片都是首次加载。后来我们在APP启动时,用Glide的preload()
方法预加载20个高频商品的图片到内存和磁盘:
Kotlin
Glide.with(context)
.load(hotProductImageUrl)
.diskCacheStrategy(DiskCacheStrategy.DATA) // 提前缓存原始数据
.preload(500, 500)
同时让服务端给这些图片设置max-age=604800
(缓存一周)。优化后,详情页图片加载时间降到400毫秒,网络请求量减少了70%。
面试官: 如果遇到CDN节点缓存和客户端缓存不一致的情况,你们怎么解决?
候选人: 这个问题比较棘手。我们遇到过用户换了头像,但因为CDN节点缓存未刷新,部分客户端还是显示旧图。当时的解决方案是双管齐下:
- 客户端降级策略 :在URL后添加动态参数,比如用户信息更新时间戳:
https://cdn.xxx.com/avatar.jpg?last_updated=1620000000
每次用户更新头像,这个参数就会变化,相当于绕过CDN缓存。 - 服务端配合:让CDN设置较短的缓存时间(比如5分钟),同时主动刷新CDN缓存。比如用户上传头像后,服务端调用CDN的Purge API清理旧缓存。
在Glide层还加了保险措施------用DiskCacheStrategy.NONE
跳过磁盘缓存,确保客户端立即用新URL请求:
Glide.with(context)
.load(avatarUrlWithTimestamp)
.diskCacheStrategy(DiskCacheStrategy.NONE) // 不缓存到磁盘
.skipMemoryCache(true) // 不缓存到内存
.into(imageView);
虽然牺牲了一点性能,但保证了关键信息的实时性。
(补充技术点,用于应对深度追问)
- 缓存清理时机 :Glide默认不会自动清理网络缓存,需要自己定期调用
okhttpClient.cache().evictAll()
或在拦截器里判断缓存大小。 - 大文件缓存 :对于视频封面等大图,建议用
DiskCacheStrategy.DATA
只缓存原始数据,避免解码后的Bitmap撑大缓存。 - HTTPS缓存 :OkHttp默认支持HTTPS响应缓存,但需要服务端配置
Cache-Control
,不能有no-store
头。
Glide与Picasso核心区别
面试官: 我看你简历里提到用过Glide和Picasso,能说说它们的区别吗?
候选人: 好的,这两个库我都用在不同的项目里,最大的区别其实是场景适应能力 。比如我之前维护过一个工具类App,APK体积要求很严格,就选了Picasso。因为它只有100多KB,代码也简单,像加载头像这种基础需求,用Picasso一行链式调用就能搞定。但后来做电商项目时,发现Picasso有个硬伤------GIF只能显示第一帧。比如商品详情页的促销动画,用Picasso加载后直接变成静态图,用户体验很糟糕,这时候只能换成Glide,因为它原生支持GIF、WebP这些复杂格式,甚至能硬解码AVIF节省流量。
面试官: 你提到电商项目用Glide,那它的优势具体体现在哪?
候选人: 最让我省心的其实是缓存策略 。比如商品列表页的图片,每个Item的ImageView尺寸不一样,Picasso会把原图全尺寸缓存下来,每次显示不同尺寸都要重新裁剪。像我们有个瀑布流页面,图片尺寸从100x100到300x300不等,用Picasso时CPU占用直接飙到70%,列表滑动明显卡顿。后来切到Glide,发现它是按显示尺寸生成缓存的。比如一个商品图要显示成200x200,Glide会直接缓存这个尺寸的图,下次同样的需求直接取缓存,CPU占用降到了30%以下,滑动流畅多了。
面试官: 除了缓存,还有其他实际体验差异吗?
候选人: 有的,内存泄漏风险 让我印象深刻。之前用Picasso时,在RecyclerView里快速滑动列表,内存一直涨,最后OOM崩溃了。排查发现是Picasso没自动绑定生命周期,页面销毁时得手动调cancelRequest()
,但我们有个老页面忘了加这个逻辑。换成Glide后,直接用Glide.with(this)
绑定Activity,页面退出时自动取消请求,内存曲线立马平稳了。像电商App这种页面跳转频繁的场景,Glide这种"保姆级"生命周期管理确实更省心。
面试官: 那什么情况下你会建议用Picasso?
候选人: 如果是轻量级工具类App ,比如计算器、天气App,图片需求简单(比如只显示静态图标),Picasso更合适。我们有个内部工具App,APK体积要求控制在5MB以内,用Picasso比Glide省了400多KB,而且代码更简洁。但一旦涉及到动态内容 (比如社区帖子的动图评论)或者性能敏感场景 (比如电商瀑布流),Glide的优势就碾压了。不过要注意,Glide的体积较大,如果项目里用ProGuard混淆没配好,可能会增加包体积,这时候得在Gradle里针对性地做代码裁剪。
面试官: 如果让你用一句话总结选型逻辑?
候选人: 我的经验是:"小项目求简用Picasso,大项目求稳用Glide"。比如上周我们讨论一个新启动的社交项目,需要支持用户上传GIF表情包,我直接拍板用Glide------虽然它体积大,但能节省后期开发动效组件的成本。反过来,如果是一个只需要加载本地静态资源的备忘录App,用Picasso反而更干净利落,不会引入不必要的依赖。
Bitmap专题
面试官: 你在项目里处理过图片加载吧?说说Bitmap在Android里是怎么工作的?
候选人: 嗯,Bitmap确实是图片处理的核心。比如我们之前做电商App的商品详情页,加载高清大图时经常要和Bitmap打交道。简单来说,Bitmap就像个像素容器------把图片解码成一个个像素点存到内存里。比如一张1000x1000的图片用ARGB_8888格式加载,差不多占4MB内存。不过实际开发中可不能这么粗放,我们吃过亏的。
有次加载用户上传的4K壁纸,没做压缩直接解码,结果OOM崩了。后来学乖了,先用BitmapFactory.Options
里的inJustDecodeBounds
先读图片尺寸,算好缩放比例再加载。比如要显示在200x200的ImageView里,就设置inSampleSize=4
,把原图缩成500x500再处理,内存直接降到原来的1/16。
面试官: 听起来你们遇到过内存问题?怎么解决的?
候选人: 是啊,刚开始做图片社交功能的时候,用户上传的图片质量参差不齐。有次测试反馈列表滑动越来越卡,用Android Profiler一查,发现Bitmap内存只涨不降。后来发现是页面跳转时没及时释放资源。
现在我们会在Fragment的onDestroyView
里调Glide.with(this).clear(imageView)
,让Glide自动回收关联的Bitmap。如果是自己手写Bitmap加载的话,得特别注意recycle()
和WeakReference
配合使用。比如生成二维码的页面,我们会用弱引用持有Bitmap,这样内存紧张时系统能自动回收,避免OOM。
面试官: 能举个实际优化Bitmap用法的例子吗?
候选人: 有的,之前做图片编辑功能时,发现滤镜处理特别耗内存。比如用户选了一张3000x4000的照片,直接Bitmap.createBitmap
会吃掉48MB内存。后来我们做了两件事:
- 预处理压缩 :先按屏幕尺寸缩放,比如手机屏幕是1080x1920,就用
inSampleSize=2
把原图压到1500x2000 - 复用Bitmap内存 :用
Bitmap.copy
配合MutableBitmap
,避免反复创建新对象
这样处理后,内存峰值从200MB降到了80MB。还有个取巧的办法------如果图片不需要透明通道,强制用RGB_565
格式,内存直接减半,虽然颜色差点,但用户基本看不出区别。
面试官: 如果让你教新人避免Bitmap的坑,你会强调哪几点?
候选人: 我肯定会拿我们踩过的雷举例:
- 尺寸!尺寸!尺寸! 重要的事说三遍。加载前一定要算
inSampleSize
,别让后端传什么就加载什么。我们曾因为加载手机相册里的8000x8000原图,把整个详情页搞崩了 - 及时回收别手软 :特别是相机、相册相关的页面,用
recycle()
+WeakReference
双保险。之前有个拍照上传功能忘了回收,用户传10张照片内存就爆了 - 格式选择要聪明 :用
inPreferredConfig
主动降级,比如缩略图用RGB_565
,大图预览用ARGB_8888
。这个优化让我们APK的OOM率降了60%
面试官: 现在很多图片库都封装了Bitmap处理,为什么还要了解底层?
候选人: 这个问题我们团队讨论过。虽然Glide这些库很好用,但遇到复杂需求还是得懂原理。比如上次要做自定义圆形渐变边框,Glide的RoundedCorners
不满足需求,我们就得自己写BitmapShader
,用Canvas
画到新的Bitmap上。
还有次用户反馈某些机型图片模糊,最后发现是ROM修改了Bitmap缓存策略。如果我们只会调API,这种问题根本无从下手。所以我觉得,会用框架是基础,懂原理才能解决真问题。
Bitmap在Glide中的作用
面试官: 你刚刚提到Glide用到了BitmapPool,那Bitmap在Glide里到底扮演什么角色?
候选人: Bitmap其实是Glide的"子弹"------所有图片加载的终点都是生成一个适配场景的Bitmap。我举个实际例子吧,之前我们做商品瀑布流,用户滑动时会频繁加载不同尺寸的图片,Glide在这过程中对Bitmap的优化让我印象深刻:
1. 内存复用:让子弹重复上膛
Glide内部有个BitmapPool (位图池),相当于一个"弹药库"。比如用户滑动列表时,上一个商品的200x200缩略图Bitmap会被回收到池子里,当加载下一个同样尺寸的商品图时,直接复用这块内存,而不是重新申请。
案例:我们曾对比过,关闭BitmapPool后列表滑动时的GC次数增加了3倍,内存波动从±5MB变成±50MB,明显卡顿。
2. 尺寸裁剪:一发子弹打准目标
Glide会根据ImageView的尺寸自动调整Bitmap大小。比如商品原图是1000x1000,但列表项只需要200x200:
- 先计算采样率(
inSampleSize=4
),把原图解码成250x250 - 再用
Bitmap.createScaledBitmap
压缩到200x200 - 最终缓存的是200x200的Bitmap,而不是原图
优化效果:同样加载100张图,内存占用从400MB降到16MB。
3. 格式转换:轻量化弹药
Glide会根据场景自动选择Bitmap格式:
- 缩略图 用
RGB_565
(无透明通道,内存省50%) - 高清大图 用
ARGB_8888
(保留透明度) - WebP动图 用硬件解码的
HARDWARE
格式(Android 8.0+)
踩坑经历:之前强制所有图片用ARGB_8888,OOM率飙升,后来让Glide自动选择,内存峰值降了40%。
4. 生命周期管理:及时回收弹壳
Glide通过RequestManager
绑定Activity/Fragment生命周期:
- 页面不可见时暂停加载
- 页面销毁时自动回收 关联的Bitmap
对比实验:用原生Bitmap加载,页面跳转时容易内存泄漏;用Glide后,内存泄漏率从15%降到0.3%。
面试官: 如果不用Glide,自己实现这些功能难点在哪?
候选人: 三个字------防不住。我们尝试过手写图片加载框架:
- 内存抖动:频繁创建/回收Bitmap导致GC频繁,列表滑动像PPT
- 尺寸计算:要自己处理屏幕密度、横竖屏适配,代码写了几百行
- 格式兼容 :不同Android版本对WebP/HEIF的支持差异巨大
最后发现,Glide用5行代码解决的问题,自己写要500行,还埋了一堆坑。现在团队共识:不要重复造轮子,除非业务有极端定制需求。
项目追问:
面试官 :
"你在简历里提到,在山海经儿童益智APP中优化了图片加载,用到了Glide的自定义三级缓存。能具体说说你是怎么做的吗?为什么要用自定义缓存而不是Glide默认的方案?"
你的回答
"好呀!其实这个需求是因为我们的APP里有大量高清神兽插图,比如麒麟、饕餮这些,很多还是动态的GIF。刚开始用Glide默认缓存时,发现低端机上容易卡顿,高端机又有点浪费内存。所以我们就针对儿童场景的特殊性,改了一套缓存策略。"
(先说怎么做)
"具体来说,分了三层优化:
- 内存缓存:Glide默认是固定比例分配内存,但像千元机(比如4GB内存)加载大图容易OOM,我们改成了动态分配------根据手机总内存分档,比如4GB手机给300MB,12GB手机给1GB。另外对大图做了'权重',比如2000px以上的图占双倍缓存空间,避免小图被大图挤掉。
- 磁盘缓存:我们把用户常看的'热门神兽'和冷门的'上古异兽'分开存。热门图放高速存储区(比如手机内置SSD),冷门图放外置SD卡,这样冷门图不会占着高速存储的空间。
- 网络加载 :小朋友滑动卡片时,如果速度很快,我们会先加载缩略图,等停下来再换成高清图。这个用的是Glide的
thumbnail()
和priority
参数,根据滑动速度动态调整。"
(再说为什么)
"至于为什么不用默认方案------主要是业务场景特殊。比如:
- 儿童用户习惯:小朋友喜欢快速翻卡片,默认缓存容易加载很多'不可见图',浪费流量还卡顿;
- 图片类型复杂:有些图是教育类(比如神兽解剖图),需要长期缓存;有些是活动图(比如节日皮肤),一周后就得失效;
- 设备差异大:家长用的手机从千元机到旗舰机都有,默认的'一刀切'缓存容易在低端机上OOM,高端机又发挥不出优势。"
(举个具体例子)
"比如'麒麟'这种热门神兽,它的插图会被高频访问。我们通过监控发现,默认缓存下,麒麟图可能被后面加载的冷门图挤掉,导致每次打开都要重新下载。改成冷热分区后,麒麟图始终留在高速存储区,小朋友秒点秒开,体验流畅了很多。"
(收尾升华)
"其实有点像图书馆的管理------常借的书放门口书架(内存缓存),冷门书放仓库(磁盘冷区),新书到货了先放展示区(预加载)。这样既能节省空间,又能让用户快速拿到最需要的书。"
面试官可能的追问
-
"动态内存分配怎么实现的?会不会影响其他功能?"
- "我们是重写了Glide的
LruCache
,在sizeOf()
方法里根据图片尺寸动态计算权重,同时限制单张图片不超过缓存总大小的1/5,防止一张图霸占整个缓存。"
- "我们是重写了Glide的
-
"冷热分区怎么判断哪些是热数据?"
- "简单版是用'最近7天访问次数',比如访问3次以上算热数据;复杂版是接入了用户行为埋点,比如停留时长、点击率,甚至根据小朋友的年龄(比如3岁爱看彩色图,8岁爱看科普图)做个性化缓存。"
-
"你们怎么验证优化效果?"
- "上线前后对比了三个数据:低端机的FPS波动从35%降到12%,图片加载流量节省了40%,家长投诉'卡顿'的工单减少了80%。"