LruCache (Least Recently Used Cache) 是基於 LRU 淘汰策略的快取實作,廣泛用於 Android 圖片快取、記憶體敏感場景。本文深度解析其核心原理、原始碼實作細節、使用最佳實踐,並提供生產級擴展方案。
訪問順序:A → B → C → D → A → E
│
▼
淘汰順序:B (最久未訪問) → C → D → A → E (最近訪問)
資料結構:HashMap + 雙向鏈表
- HashMap: O(1) 查找 key → 節點
- 雙向鏈表: 維護訪問順序,頭部=最新,尾部=最舊
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map; // 底層容器,accessOrder=true
private int size; // 當前總大小 (bytes 或 count)
private int maxSize; // 最大容量限制
// 統計計數器
private int putCount, createCount, evictionCount, hitCount, missCount;
}
// LinkedHashMap 建構子
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder; // true = 訪問順序,false = 插入順序
}
// 當 accessOrder=true 時:
// - get(key) 會將節點移至鏈表尾部 (最新)
// - put(key, value) 新增節點至尾部
// - 迭代順序:頭部(最舊) → 尾部(最新)
// - removeEldestEntry() 可控制淘汰邏輯
public final V get(K key) {
if (key == null) throw new NullPointerException("key == null");
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue; // 命中:LinkedHashMap 內部已移至尾部
}
missCount++;
}
// 未命中:嘗試 create()
V createdValue = create(key);
if (createdValue == null) return null;
synchronized (this) {
createCount++;
// 雙重檢查:create 期間可能被其他執行緒 put
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// 衝突:使用舊值,釋放新建值
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize); // 可能觸發淘汰
return createdValue;
}
}
public final V put(K key, V value) {
if (key == null || value == null) throw new NullPointerException();
V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value); // LinkedHashMap 移至尾部
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value); // 替換觸發
}
trimToSize(maxSize); // 關鍵:檢查是否超容量
return previous;
}
private void trimToSize(int maxSize) {
while (true) {
K key; V value;
synchronized (this) {
if (size <= maxSize || map.isEmpty()) break;
// 淘汰頭部 (最舊)
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null); // 真正淘汰
}
}
// 預設:每個條目大小 = 1 (計數模式)
protected int sizeOf(K key, V value) { return 1; }
// 圖片快取範例:按位元組計算
LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(maxSizeBytes) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount(); // 或 value.getAllocationByteCount() (API 19+)
}
};
public class ImageCache {
private static final int MAX_MEMORY = (int) (Runtime.getRuntime().maxMemory() / 1024); // KB
private static final int CACHE_SIZE = MAX_MEMORY / 8; // 1/8 記憶體,建議 1/4 ~ 1/8
private final LruCache<String, Bitmap> memoryCache;
public ImageCache() {
memoryCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024; // KB
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
if (evicted) {
// 可選:記錄淘汰日誌、釋放原生記憶體
Log.d("ImageCache", "Evicted: " + key + ", size: " + sizeOf(key, oldValue) + "KB");
}
}
};
}
public void put(String key, Bitmap bitmap) {
if (get(key) == null) {
memoryCache.put(key, bitmap);
}
}
public Bitmap get(String key) {
return memoryCache.get(key);
}
}
public class DualCache {
private final LruCache<String, Bitmap> memoryCache;
private final DiskLruCache diskCache; // JakeWharton/DiskLruCache
public Bitmap get(String key) {
// L1: 記憶體快取
Bitmap bitmap = memoryCache.get(key);
if (bitmap != null) return bitmap;
// L2: 磁碟快取
try {
DiskLruCache.Snapshot snapshot = diskCache.get(key);
if (snapshot != null) {
InputStream is = snapshot.getInputStream(0);
bitmap = BitmapFactory.decodeStream(is);
if (bitmap != null) {
memoryCache.put(key, bitmap); // 回填記憶體
}
return bitmap;
}
} catch (IOException e) {
Log.e("DualCache", "Disk read error", e);
}
return null;
}
public void put(String key, Bitmap bitmap) {
memoryCache.put(key, bitmap);
// 非同步寫入磁碟
executor.execute(() -> {
try {
DiskLruCache.Editor editor = diskCache.edit(key);
if (editor != null) {
OutputStream os = editor.newOutputStream(0);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
editor.commit();
}
} catch (IOException e) {
Log.e("DualCache", "Disk write error", e);
}
});
}
}
主流圖片庫內部皆使用 LruCache 作為記憶體快取層:
MemoryCache 介面,預設實作 LruResourceCache (包裝 LruCache)LruCache 直接作為快取LruCache + 磁碟快取
public class TimedLruCache<K, V> extends LruCache<K, V> {
private final Map<K, Long> timestamps = new ConcurrentHashMap<>();
private final long ttlMillis;
public TimedLruCache(int maxSize, long ttlMillis) {
super(maxSize);
this.ttlMillis = ttlMillis;
}
@Override
public V get(K key) {
Long ts = timestamps.get(key);
if (ts != null && System.currentTimeMillis() - ts > ttlMillis) {
remove(key); // 過期移除
timestamps.remove(key);
return null;
}
return super.get(key);
}
@Override
public V put(K key, V value) {
timestamps.put(key, System.currentTimeMillis());
return super.put(key, value);
}
@Override
public V remove(K key) {
timestamps.remove(key);
return super.remove(key);
}
}
public interface CacheListener<K, V> {
void onHit(K key, V value);
void onMiss(K key);
void onEvict(K key, V value, boolean evicted);
void onPut(K key, V value);
}
public class ObservableLruCache<K, V> extends LruCache<K, V> {
private final List> listeners = new CopyOnWriteArrayList<>();
public void addListener(CacheListener listener) { listeners.add(listener); }
public void removeListener(CacheListener listener) { listeners.remove(listener); }
@Override
public V get(K key) {
V value = super.get(key);
if (value != null) listeners.forEach(l -> l.onHit(key, value));
else listeners.forEach(l -> l.onMiss(key));
return value;
}
@Override
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
listeners.forEach(l -> l.onEvict(key, oldValue, evicted));
}
}
public class CacheStats {
private final LruCache, ?> cache;
public CacheStats(LruCache, ?> cache) { this.cache = cache; }
public double hitRate() {
int total = cache.hitCount() + cache.missCount();
return total == 0 ? 0 : (double) cache.hitCount() / total;
}
public String report() {
return String.format(
"LruCache Stats: maxSize=%d, currentSize=%d, puts=%d, hits=%d, misses=%d, evictions=%d, hitRate=%.2f%%",
cache.maxSize(), cache.size(), cache.putCount(), cache.hitCount(),
cache.missCount(), cache.evictionCount(), hitRate() * 100
);
}
}
// 定時上報
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> Log.i("CacheMonitor", stats.report()), 1, 1, TimeUnit.MINUTES);
| 優化點 | 建議 | 效果 |
|---|---|---|
| 初始容量 | 預估熱點資料量,設定 initialCapacity 避免擴容 | 減少 HashMap resize |
| 併發鎖 | 高併發考慮 ConcurrentLinkedHashCache 或 Caffeine | 降低鎖競爭 |
| 大小計算 | 避免複雜 sizeOf(),Bitmap 用 getByteCount() 即可 | 降低 CPU 開銷 |
| 批量操作 | 使用 putAll() 替代迴圈 put() | 減少同步開銷 |
| 弱引用值 | 配合 ReferenceQueue 實現更激進淘汰 | 記憶體更緊湊 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| LruCache (Android) | 輕量、無依賴、系統整合 | 單機、單執行緒鎖、功能簡單 | Android App 記憶體快取 |
| Caffeine | 高效能、W-TinyLFU、豐富功能 | JVM only、體積較大 | Java 服務端、高併發快取 |
| Guava Cache | 功能完整、文檔豐富 | 效能不如 Caffeine、維護模式 | 舊專案遷移 |
| Redis | 分散式、持久化、多語言 | 網路延遲、序列化開銷 | 多實例共享快取 |
| Ehcache | 堆外記憶體、磁碟持久化 | 配置複雜、效能一般 | 企業級應用、Hibernate 整合 |
LruCache 以 LinkedHashMap(accessOrder=true) + synchronized 實現高效 LRU 快取,核心優勢在於O(1) 存取、自動淘汰、可自訂大小計算。Android 開發中是圖片快取首選;服務端高併發場景建議升級 Caffeine。掌握 sizeOf()、entryRemoved()、create() 三個擴展點,即可應對大多數進階需求。