首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 快取利器 LruCache 深度解析:原理、實戰與 Android 實作

     編輯:Java 相關知識

前言

LruCache (Least Recently Used Cache) 是基於 LRU 淘汰策略的快取實作,廣泛用於 Android 圖片快取、記憶體敏感場景。本文深度解析其核心原理、原始碼實作細節、使用最佳實踐,並提供生產級擴展方案。

LRU 算法核心原理

訪問順序:A → B → C → D → A → E
                    │
                    ▼
淘汰順序:B (最久未訪問) → C → D → A → E (最近訪問)

資料結構:HashMap + 雙向鏈表
- HashMap: O(1) 查找 key → 節點
- 雙向鏈表: 維護訪問順序,頭部=最新,尾部=最舊

Android LruCache 原始碼深度解析

核心欄位


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 的 accessOrder


// 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() 可控制淘汰邏輯

get() 核心流程


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;
    }
}

put() 與淘汰機制


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);  // 真正淘汰
    }
}

sizeOf() 可自訂大小計算


// 預設:每個條目大小 = 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+)
    }
};

實戰:Android 圖片快取最佳實踐

1. 快取配置策略


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);
    }
}

2. 結合磁碟快取


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);
            }
        });
    }
}

3. Glide/Picasso 整合原理

主流圖片庫內部皆使用 LruCache 作為記憶體快取層:

  • Glide:MemoryCache 介面,預設實作 LruResourceCache (包裝 LruCache)
  • Picasso:LruCache 直接作為快取
  • Coil:Kotlin 協程 + LruCache + 磁碟快取

進階擴展:生產級功能增強

1. 過期時間支援 (TTL)


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);
    }
}

2. 快取事件監聽


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));
    }
}

3. 統計監控指標


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 整合

常見坑點

坑 1:Bitmap 重複快取導致 OOM
同一圖片不同 key (如 URL 帶參數) 會重複存入。解決:key 正規化、使用內容哈希作為 key。
坑 2:sizeOf() 返回負值或 0
導致 trimToSize() 死循環或淘汰失效。必須確保 sizeOf() > 0。
坑 3:create() 中做耗時操作
create() 在同步鎖外執行但會阻塞其他 get()。耗時操作應異步預載。
坑 4:不釋放 Bitmap 原生記憶體
API < 19 需手動 recycle(),配合 entryRemoved() 釋放。
坑 5:靜態快取導致記憶體洩漏
Activity/Fragment 持有靜態 LruCache 導致無法 GC。使用 Application Context 或單例模式。

總結

LruCache 以 LinkedHashMap(accessOrder=true) + synchronized 實現高效 LRU 快取,核心優勢在於O(1) 存取、自動淘汰、可自訂大小計算。Android 開發中是圖片快取首選;服務端高併發場景建議升級 Caffeine。掌握 sizeOf()、entryRemoved()、create() 三個擴展點,即可應對大多數進階需求。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved