一、為什么需要三級緩存
內存緩存(Memory Cache)
內存緩存旨在快速顯示剛瀏覽過的圖片,例如在滑動列表時來回切換的圖片。在 Glide 中,內存緩存使用 LruCache 算法(最近最少使用),能自動清理長時間未使用的圖片,以此確保內存的合理利用。通常,內存緩存限制在手機可用內存的 15%。舉例來說,若手機擁有 8GB 內存,內存緩存大約為 1.2GB。同時,為了進一步優化,圖片會按屏幕尺寸進行壓縮,比如原圖為 2000px,而手機屏幕為 1000px,那么只存儲 1000px 版本的圖片。當內存緩存超出限制時,會自動清理超出部分的圖片。
代碼實現:
import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.LruResourceCache;
import com.bumptech.glide.module.AppGlideModule;public class CustomGlideModule extends AppGlideModule {@Overridepublic void applyOptions(Context context, GlideBuilder builder) {// 獲取設備的最大內存int maxMemory = (int) Runtime.getRuntime().maxMemory();// 計算內存緩存的大小,這里設置為最大內存的15%int memoryCacheSize = maxMemory / 1024 / 1024 * 15;// 創建LruResourceCache對象builder.setMemoryCache(new LruResourceCache(memoryCacheSize));}
}
磁盤緩存(Disk Cache)
磁盤緩存用于存儲常用但當前不在內存中的圖片,像用戶經常訪問的商品詳情頁圖片。Glide 通過 DiskLruCache 將圖片存儲在手機硬盤上,總容量一般設置為 100MB,并且優先存儲高質量圖片。為了優化存儲,圖片按 URL 哈希值命名文件,這樣可以避免重復存儲相同圖片。同時,對于超過 7 天未使用的圖片,會自動進行清理。
代碼實現:
import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.DiskLruCacheFactory;
import com.bumptech.glide.module.AppGlideModule;public class CustomDiskCacheGlideModule extends AppGlideModule {@Overridepublic void applyOptions(Context context, GlideBuilder builder) {// 設置磁盤緩存的路徑String diskCachePath = context.getCacheDir().getPath() + "/glide_cache";// 設置磁盤緩存的大小為100MBint diskCacheSize = 1024 * 1024 * 100;// 創建DiskLruCacheFactory對象builder.setDiskCache(new DiskLruCacheFactory(diskCachePath, diskCacheSize));}
}
網絡緩存(Network Cache)
網絡緩存的作用是避免重復從服務器下載相同圖片,這需要結合 HTTP 緩存頭來實現。Glide 借助 OkHttp 的緩存機制,將圖片存儲在路由器或基站緩存中,總容量設置為 50MB,優先存儲高頻訪問的圖片。通過根據 HTTP 的 Cache-Control 頭設置緩存時間(例如設置為 1 天),以及在圖片 URL 中添加版本號(如 image_v2.jpg),當版本更新時強制重新下載,從而實現高效的網絡緩存管理。
代碼實現:
import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.module.AppGlideModule;
import okhttp3.Cache;
import okhttp3.OkHttpClient;import java.io.InputStream;public class CustomNetworkCacheGlideModule extends AppGlideModule {@Overridepublic void registerComponents(Context context, Glide glide, Registry registry) {// 設置網絡緩存的路徑Cache cache = new Cache(context.getCacheDir(), 1024 * 1024 * 50);// 創建OkHttpClient對象并設置緩存OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();// 注冊OkHttpUrlLoader,讓Glide使用OkHttp進行網絡請求registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client));}
}
整合代碼:?
import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.LruResourceCache;
import com.bumptech.glide.module.AppGlideModule;/*** 自定義Glide內存緩存配置* 通過LruCache算法實現最近最少使用的圖片自動回收*/
public class CustomGlideModule extends AppGlideModule {@Overridepublic void applyOptions(Context context, GlideBuilder builder) {// 獲取應用可使用的最大內存(單位:字節)int maxMemory = (int) Runtime.getRuntime().maxMemory();// 計算內存緩存大小(15%的可用內存)int memoryCacheSize = maxMemory / 1024 / 1024 * 15;// 創建LruResourceCache并設置緩存大小builder.setMemoryCache(new LruResourceCache(memoryCacheSize));}
}import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.DiskLruCacheFactory;
import com.bumptech.glide.module.AppGlideModule;/*** 自定義Glide磁盤緩存配置* 使用DiskLruCache將圖片持久化到本地存儲*/
public class CustomDiskCacheGlideModule extends AppGlideModule {@Overridepublic void applyOptions(Context context, GlideBuilder builder) {// 設置磁盤緩存路徑(應用緩存目錄下的glide_cache文件夾)String diskCachePath = context.getCacheDir().getPath() + "/glide_cache";// 設置磁盤緩存大小(100MB)int diskCacheSize = 1024 * 1024 * 100;// 創建DiskLruCache工廠并設置路徑和大小builder.setDiskCache(new DiskLruCacheFactory(diskCachePath, diskCacheSize));}
}import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.module.AppGlideModule;
import okhttp3.Cache;
import okhttp3.OkHttpClient;import java.io.InputStream;/*** 自定義Glide網絡緩存配置* 結合OkHttp實現HTTP級別的網絡緩存*/
public class CustomNetworkCacheGlideModule extends AppGlideModule {@Overridepublic void registerComponents(Context context, Glide glide, Registry registry) {// 創建OkHttp緩存(50MB,位于應用緩存目錄)Cache cache = new Cache(context.getCacheDir(), 1024 * 1024 * 50);// 構建帶緩存的OkHttpClientOkHttpClient client = new OkHttpClient.Builder().cache(cache).build();// 注冊OkHttp為Glide的網絡請求引擎registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client));}
}
常見問題解決方案
- 緩存穿透:緩存穿透指查詢一個一定不存在的數據,由于緩存不命中需要從數據庫查詢,查不到數據則不寫入緩存,導致該不存在的數據每次請求都要到數據庫查詢,給數據庫帶來壓力。在 Glide 中,可以通過設置錯誤占位圖、加載占位圖和空值占位圖來解決部分問題。例如:
Glide.with(context).load(url).error(R.drawable.ic_error) // 設置錯誤占位圖.placeholder(R.drawable.ic_loading) // 設置加載占位圖.fallback(R.drawable.ic_fallback) // 設置空值占位圖.into(imageView);
在大廠面試中,關于緩存穿透常被問到的問題有:“請簡述緩存穿透的概念以及可能的解決方案”。回答時,除了像上述代碼那樣通過 Glide 的占位圖設置來應對外,還可以提及如使用布隆過濾器(Bloom Filter)等方案。布隆過濾器是一種空間效率極高的概率型數據結構,它利用位數組和哈希函數來判斷一個元素是否在一個集合中。將所有已存在的數據 key 放入布隆過濾器中,當新的請求到來時,先通過布隆過濾器判斷該 key 是否存在。如果不存在,直接返回,避免查詢數據庫,從而有效減少不必要的數據庫查詢,提高系統性能。
- 緩存雪崩:緩存雪崩是指在某一時刻,大量緩存同時失效,導致大量請求直接訪問數據庫,造成數據庫壓力過大甚至崩潰。可以通過設置不同的緩存過期時間來避免,例如:
int cacheDuration = TimeUnit.HOURS.toMillis(24) + new Random().nextInt(3600000);
面試中可能會被問到:“如何防止緩存雪崩的發生”。除了上述設置隨機過期時間的方法外,還可以采用二級緩存策略,即設置主緩存和備用緩存。主緩存失效后,先從備用緩存獲取數據,同時對主緩存進行異步更新,這樣可以在一定程度上緩解大量請求直接沖擊數據庫的問題。另外,使用互斥鎖也是一種思路,在緩存失效時,只有一個線程能夠獲取鎖去更新緩存,其他線程等待,避免大量線程同時查詢數據庫。
- OOM 預防:OOM(Out Of Memory,內存溢出)在圖片加載中較為常見,因為圖片占用內存較大。可以通過使用 RGB_565 格式減少內存占用,例如:
// 使用RGB_565格式減少內存占用
Glide.with(context).load(url).format(DecodeFormat.PREFER_RGB_565).into(imageView);
面試官可能會問:“在 Glide 中,如何預防 OOM 問題”。除了設置圖片格式外,還可以根據設備內存情況動態調整圖片尺寸。例如,獲取設備的可用內存,當內存較低時,對圖片進行更大比例的壓縮。同時,合理配置 Glide 的內存緩存大小也很關鍵,避免緩存占用過多內存。此外,及時釋放不再使用的圖片資源,Glide 通過與 Activity 或 Fragment 的生命周期綁定,在界面不可見時及時清理相關圖片資源,防止內存泄漏。
關鍵指標的獲取途徑
- 冷啟動加載時間:借助 Android Profiler 的 Timeline 功能來精準測量。在應用啟動時,啟動 Profiler 并記錄圖片加載所耗費的時長。代碼示例如下:
long startTime = System.currentTimeMillis();
Glide.with(this).load(url).into(imageView);
long duration = System.currentTimeMillis() - startTime;
Log.d("GlideTest", "加載耗時: " + duration + "ms");
- 內存峰值占用情況:使用 Android Profiler 的 Memory Monitor 進行監測。在滑動列表時,留意 Heap Size 的變化趨勢,對比開啟緩存前后 Bitmap 內存占用的差異,以此來優化內存使用。
- 緩存命中率計算:通過 Glide 的日志輸出(設置 Glide.get (context).setLogLevel (Log.DEBUG)),從日志中篩選出 Fetched 和 Decoded 相關的條目。緩存命中率 = (內存命中數 + 磁盤命中數)÷ 總請求數 × 100%。
- FPS 幀率監控:采用 Android Profiler 的 FrameMetrics 功能。在滑動列表的過程中,記錄丟幀的數量,確保平均幀率穩定在 55fps 以上,以保證流暢的用戶體驗。
二、自定義圖片緩存框架
設計思路
- 內存緩存:運用 LruCache(Least Recently Used Cache,最近最少使用緩存)實現內存緩存,它能夠自動回收最近最少使用的圖片,保障內存的合理使用。
- 磁盤緩存:利用 DiskLruCache 實現磁盤緩存,將圖片持久化到本地磁盤,方便在網絡不可用或需要重復使用圖片時快速獲取。
- 多級緩存策略:首先從內存緩存中查找圖片,若未找到則從磁盤緩存中查找,最后才從網絡請求圖片。當從網絡獲取到圖片后,同時將其存入內存緩存和磁盤緩存。
代碼實現
import android.graphics.Bitmap;
import android.util.LruCache;/*** 內存緩存實現* 使用LruCache(最近最少使用)算法管理內存中的圖片*/
public class MemoryCache {// LruCache實例,用于存儲圖片(鍵為圖片URL,值為Bitmap)private LruCache<String, Bitmap> lruCache;public MemoryCache() {// 獲取應用最大可用內存(KB)int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);// 設置緩存大小為最大內存的1/8int cacheSize = maxMemory / 8;// 初始化LruCache并重寫sizeOf方法計算每個Bitmap的大小lruCache = new LruCache<String, Bitmap>(cacheSize) {@Overrideprotected int sizeOf(String key, Bitmap bitmap) {// 返回Bitmap占用的內存大小(KB)return bitmap.getByteCount() / 1024;}};}// 向緩存添加圖片public void put(String key, Bitmap bitmap) {if (get(key) == null) {lruCache.put(key, bitmap);}}// 從緩存獲取圖片public Bitmap get(String key) {return lruCache.get(key);}// 從緩存移除圖片public void remove(String key) {lruCache.remove(key);}
}import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import com.jakewharton.disklrucache.DiskLruCache;import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;/*** 磁盤緩存實現* 使用DiskLruCache將圖片持久化到本地存儲*/
public class DiskCache {// 應用版本(用于緩存版本控制)private static final int APP_VERSION = 1;// 每個緩存項對應的值數量private static final int VALUE_COUNT = 1;// 磁盤緩存最大容量(10MB)private static final long CACHE_SIZE = 10 * 1024 * 1024;// DiskLruCache實例private DiskLruCache diskLruCache;public DiskCache(Context context) {try {// 獲取緩存目錄File cacheDir = getDiskCacheDir(context, "bitmap");if (!cacheDir.exists()) {cacheDir.mkdirs();}// 打開DiskLruCache實例diskLruCache = DiskLruCache.open(cacheDir, APP_VERSION, VALUE_COUNT, CACHE_SIZE);} catch (IOException e) {e.printStackTrace();}}// 向磁盤緩存添加圖片public void put(String key, Bitmap bitmap) {DiskLruCache.Editor editor = null;try {// 獲取緩存編輯器editor = diskLruCache.edit(hashKeyForDisk(key));if (editor != null) {// 獲取輸出流并寫入圖片(JPEG格式,質量100%)OutputStream outputStream = editor.newOutputStream(0);if (bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)) {editor.commit();} else {editor.abort();}outputStream.close();}} catch (IOException e) {e.printStackTrace();}}// 從磁盤緩存獲取圖片public Bitmap get(String key) {try {// 獲取緩存快照DiskLruCache.Snapshot snapshot = diskLruCache.get(hashKeyForDisk(key));if (snapshot != null) {// 從輸入流解碼BitmapInputStream inputStream = snapshot.getInputStream(0);return BitmapFactory.decodeStream(inputStream);}} catch (IOException e) {e.printStackTrace();}return null;}// 從磁盤緩存移除圖片public void remove(String key) {try {diskLruCache.remove(hashKeyForDisk(key));} catch (IOException e) {e.printStackTrace();}}// 獲取磁盤緩存目錄private File getDiskCacheDir(Context context, String uniqueName) {String cachePath;// 判斷外部存儲是否可用if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())||!Environment.isExternalStorageRemovable()) {cachePath = context.getExternalCacheDir().getPath();} else {cachePath = context.getCacheDir().getPath();}return new File(cachePath + File.separator + uniqueName);}// 生成URL的MD5哈希值作為緩存鍵private String hashKeyForDisk(String key) {String cacheKey;try {// 使用MD5算法生成哈希值final MessageDigest mDigest = MessageDigest.getInstance("MD5");mDigest.update(key.getBytes());cacheKey = bytesToHexString(mDigest.digest());} catch (NoSuchAlgorithmException e) {// 若不支持MD5,使用普通哈希碼cacheKey = String.valueOf(key.hashCode());}return cacheKey;}// 字節數組轉十六進制字符串private String bytesToHexString(byte[] bytes) {StringBuilder sb = new StringBuilder();for (byte b : bytes) {String hex = Integer.toHexString(0xFF & b);if (hex.length() == 1) {sb.append('0');}sb.append(hex);}return sb.toString();}
}import android.content.Context;
import android.graphics.Bitmap;/*** 多級緩存管理器* 統一管理內存緩存和磁盤緩存*/
public class ImageCacheManager {// 內存緩存實例private MemoryCache memoryCache;// 磁盤緩存實例private DiskCache diskCache;public ImageCacheManager(Context context) {memoryCache = new MemoryCache();diskCache = new DiskCache(context);}// 同時存入內存緩存和磁盤緩存public void put(String key, Bitmap bitmap) {memoryCache.put(key, bitmap);diskCache.put(key, bitmap);}// 優先從內存緩存獲取,再從磁盤緩存獲取public Bitmap get(String key) {Bitmap bitmap = memoryCache.get(key);if (bitmap != null) {return bitmap;}bitmap = diskCache.get(key);if (bitmap != null) {// 從磁盤讀取后存入內存,提升下次訪問速度memoryCache.put(key, bitmap);}return bitmap;}
}
-
請簡述三級緩存(內存緩存、磁盤緩存、網絡緩存)的作用和原理。
- 內存緩存:旨在快速顯示剛瀏覽過的圖片,使用 LruCache 算法(最近最少使用),自動清理長時間未使用的圖片,確保內存的合理利用。通常限制在手機可用內存的 15%。
- 磁盤緩存:用于存儲常用但當前不在內存中的圖片,通過 DiskLruCache 將圖片存儲在手機硬盤上,設置總容量(如 100MB),優先存儲高質量圖片,按 URL 哈希值命名文件以避免重復存儲,超過 7 天未使用的圖片會自動清理。
- 網絡緩存:避免重復從服務器下載相同圖片,結合 HTTP 緩存頭,借助 OkHttp 的緩存機制,將圖片存儲在路由器或基站緩存中,設置總容量(如 50MB),優先存儲高頻訪問的圖片,根據 HTTP 的 Cache-Control 頭設置緩存時間,并在圖片 URL 中添加版本號以強制重新下載。
-
在自定義圖片緩存框架中,LruCache 和 DiskLruCache 分別是如何實現的?
- LruCache:在內存緩存類中,獲取應用程序運行時的最大可用內存,使用最大可用內存的一部分(如 1/8)作為 LruCache 的緩存大小。重寫 sizeOf 方法,計算每個圖片對象占用的內存大小,通過 put 方法添加圖片到緩存,get 方法獲取圖片,remove 方法移除圖片。
- DiskLruCache:在磁盤緩存類中,初始化時獲取磁盤緩存的目錄,打開 DiskLruCache 實例。put 方法通過獲取編輯器和輸出流,將圖片以 JPEG 格式壓縮并寫入;get 方法通過獲取快照和輸入流,將輸入流解碼為 Bitmap 對象;remove 方法移除指定的圖片。對鍵進行 MD5 哈希處理,確保鍵的唯一性。
-
如何防止緩存穿透、緩存雪崩和 OOM 問題?
- 緩存穿透:在 Glide 中,可以通過設置錯誤占位圖、加載占位圖和空值占位圖來解決部分問題。另外,可以使用布隆過濾器,將所有已存在的數據 key 放入布隆過濾器中,當新的請求到來時,先通過布隆過濾器判斷該 key 是否存在,避免不必要的數據庫查詢。
- 緩存雪崩:可以通過設置不同的緩存過期時間來避免,例如在設置緩存過期時間時,添加一個隨機值。另外,采用二級緩存策略,設置主緩存和備用緩存,主緩存失效后,先從備用緩存獲取數據,同時對主緩存進行異步更新。使用互斥鎖,在緩存失效時,只有一個線程能夠獲取鎖去更新緩存,其他線程等待。
- OOM:在 Glide 中,可以使用 RGB_565 格式減少內存占用,根據設備內存情況動態調整圖片尺寸,合理配置 Glide 的內存緩存大小,避免緩存占用過多內存。及時釋放不再使用的圖片資源,Glide 通過與 Activity 或 Fragment 的生命周期綁定,在界面不可見時及時清理相關圖片資源,防止內存泄漏。