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
的時候恢復。這樣可以避免在快速滑動時,大量不可見的圖片請求搶占資源,導致當前屏幕上該顯示的圖片加載不出來。
// 在你的 Activity 或者 Fragment 中設置 RecyclerView
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {super.onScrollStateChanged(recyclerView, newState)val context = recyclerView.contextif (!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)
)來先加載一個小版本的圖片,等原圖加載好了再平滑過渡過去。這樣至少能快速展示點東西,避免白屏。
// 在 RecyclerView.Adapter 的 onBindViewHolder 方法里
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {val imageUrl = imageUrls[position] // 假設這是圖片URL列表val context = holder.imageView.contextGlide.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 做到這一點。
// 還是在 onBindViewHolder 里
Glide.with(context).load(imageUrl).override(150, 150) // 指定加載的圖片解碼到內存中的尺寸.centerCrop() // 或者 fitCenter(),根據需求來.placeholder(R.drawable.placeholder_image).into(holder.imageView)
緩存策略檢查:
????????確認內存緩存和磁盤緩存都開啟了并且配置得當。如果圖片之前加載過,快速滑回去的時候應該能從內存或者磁盤緩存里秒出,這樣就不會卡頓了。如果發現滑過的圖片再次出現時還是重新加載,那就要檢查緩存配置,或者是不是 URL 每次都變了導致緩存的 key 不一樣。
// 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 就是圖片 URLreturn 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里強制加緩存策略。比如在攔截器里統一設置緩存時間:
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()
方法也能強制刷新緩存。比如檢測到用戶信息變更時,生成一個新的簽名:
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個高頻商品的圖片到內存和磁盤:
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%。”
Android學習總結之Bitmap篇_android bitmap compress-CSDN博客https://blog.csdn.net/2301_80329517/article/details/147618477Android面試總結之Glide源碼級理解_android glide原理面試-CSDN博客
https://blog.csdn.net/2301_80329517/article/details/146602852