天天看點

從設計到實作,一步步教你實作Android-Universal-ImageLoader-緩存Cache反思

轉載請标明出處,本文出自:chaossss的部落格

Android-Universal-ImageLoader Github 位址

Cache

我們要對圖檔進行緩存,有兩種方式:記憶體緩存和本地緩存。這兩種方式的差別在于,記憶體緩存是緩存在 Android 系統為應用配置設定的運作記憶體之中,讀取速度快,但是可能會帶來 OOM 的問題;本地緩存一般緩存在 SD 卡中,讀取速度較慢,但是緩存空間足。

那麼我們要怎麼來實作記憶體緩存和本地緩存呢?根據單一職責原則,如果 MemoryCache 和 DiskCache 的抽象不一緻的話,我們就需要分别建立 MemoryCache 和 DiskCache 的抽象基類,分别實作各自的細節。而顯然,兩者抽象是不一緻的,因為 MemoryCache 面對的對象是圖檔(Bitmap),DiskCache 面對的對象是檔案(File)。是以,我們應該分開實作 MemoryCache 和 DiskCache。

MemoryCache

MemoryCache 整體設計及相關基類的實作

那我們現在該幹啥呢?想 MemoryCache 的功能啊!對于進行記憶體緩存我們能想到什麼應用場景呢:

  1. 首先每一個 MemoryCache 肯定有相應的增删取功能
  2. 當 MemoryCache 被存滿了(Android 應用的記憶體資源是很寶貴的),我們該怎麼處理呢:
    • 回收最近沒有用過的
    • 回收最早在記憶體中緩存的
    • 回收使用頻率最低的
    • 在緩存時,key 相同的圖檔,回收舊的圖檔,緩存新的圖
  3. 有時候我們可能需要緩存高分辨率的高清圖檔,而這種圖檔非常大,緩存到記憶體中就會 OOM,在這種情況下,為了讓應用能正常運作,我們應該能在緩存時限制圖檔的大小

我就想到這麼多哈,肯定還會有很多不一樣的情況,畢竟需求是層出不窮的……那麼根據現在得到的應用場景,我們就要開始設計 MemoryCache 啦。根據分析得到的結果我們可以發現:記憶體緩存有不一樣的緩存政策和緩存限制,但是具有相同的抽象。是以我們首先需要實作 MemoryCache 的抽象:

public interface MemoryCache {

    boolean put(String key, Bitmap value);

    Bitmap get(String key);

    Bitmap remove(String key);

    Collection<String> keys();

    void clear();
}
           

實作了抽象,就得開始考慮具體實作拉。我們剛剛也說了,MemoryCache 具有不同的緩存政策和緩存限制,政策不一樣的實作類一般不會存在繼承關系,而具有相同限制的 MemoryCache 則可能存在抽象。那麼我們可以得到:

很多人會奇怪了,為什麼 LruMemoryCache 和 FuzzyKeyMemoryCache 不是繼承于 BaseMemoryCache 呢?我們不妨想象為什麼需要 BaseMemoryCache,我們之是以引入 BaseMemoryCache,是因為限制不同的 MemoryCache 具有相同的抽象,在 AUImgLoader 中,不同的限制展現在:緩存圖檔的大小限制和緩存圖檔的引用方式限制。

有關引用的知識可以看這Java中的強引用、軟引用、弱引用和虛引用

是以,在 BaseMemoryCache 的實作中,我們添加了對應的抽象方法:

protected abstract Reference<Bitmap> createReference(Bitmap value);
           

反觀 LruMemoryCache,我在深入源碼剖析LruCache中給大家講解過 LruCache 的原理,我們在 LruMemoryCache 中進行緩存,是隻使用強引用進行緩存的,換言之,LruMemoryCache 的抽象和 BaseMemory 的實作是不一樣的,因為 BaseMemoryCache 中多了一個 createReference() 抽象方法,而 LruMemoryCache 不需要這個抽象方法。

而 FuzzyKeyMemoryCache 是一個隻考慮緩存政策的記憶體緩存類,它隻考慮怎麼去處理 key 相同的圖檔,具體你用什麼方式實作,就由開發者自己決定,因為 FuzzyKeyMemory 的處理方法都是調用抽象接口完成的(源碼我就不放了哈,大家可以自己下載下傳)

BaseMemoryCache

經過剛剛的分析,我們得到了 MemoryCache 的三個基類:BaseMemoryCache、LruMemoryCache、FuzzyKeyMemoryCache。接下來,我們就來根據 BaseMemoryCache 實作我們想要的細節。

我們已經提到,BaseMemoryCache 是用于處理緩存限制的基類,而所謂的限制展現在:限制大小和限制引用方式。那麼很顯然,引用方式隻有強引用、弱引用值得我們進行區分(Android 2.3 已經不鼓勵開發者使用軟引用了,因為在進行垃圾回收時軟引用和弱引用都具有被回收的傾向),是以我們可以得到下面兩個實作類:

千呼萬喚始出來,我們最終的細節實作類 LimitedMemoryCache 終于出現了……在 LimitedMemoryCache 中,我們将使用強引用緩存圖檔,那麼,LimitedMemoryCache 還需要提供的就是圖檔大小的限制:

public LimitedMemoryCache(int sizeLimit) {
        this.sizeLimit = sizeLimit;
        cacheSize = new AtomicInteger();
        if (sizeLimit > MAX_NORMAL_CACHE_SIZE) {
            L.w("You set too large memory cache size (more than %1$d Mb)", MAX_NORMAL_CACHE_SIZE_IN_MB);
        }
    }
           

剩下的,就是根據我們的緩存政策對 LimitedMemoryCache 進行拓展啦。

MemoryCahce 的工具類

那麼經過剛剛的努力,我們現在已經能過在記憶體中進行緩存,即使 AUImgLoader 自帶的緩存實作類不能滿足我們的需求,由于 AUImgLoader 的記憶體緩存功能子產品是通過裝飾者模式架構的,我們要對已有的類進行拓展也是很簡單的。但是現在記憶體緩存子產品隻是提供了必要的“記憶體緩存功能”,我們還需不需要其他的工具來簡化我們的使用呢?

大家不妨想想,我們加載了一些尺寸較小的圖檔在記憶體中,它們占用的總記憶體并不大,還不需要将它們緩存到本地,但我們仍需要對它們進行細粒度的管理(否則當圖檔數量增多,記憶體空間不夠時我們對圖檔的處理會帶來各種問題)。那麼我們就需要一個工具類協助我們進行記憶體緩存的管理,不妨建立一個叫做 MemoryCacheUtils 的類:

public final class MemoryCacheUtils {

    private MemoryCacheUtils() {
    }
}
           

大家會注意到這個類将是一個無法被繼承的類,也無法通過構造方法獲得執行個體對象。畢竟一方面,這個工具類不需要重複建立執行個體對象,隻需要調用類去執行方法就行了;另一方面,工具類并不需要繼承,需要什麼添加進去就行了。

這裡說 MemoryCacheUtils 無法建立執行個體對象是指一般情況下無法建立,實際上如果使用 Java 的反射機制的話還是可以建立對象的。

那麼這個類要幫我們完成什麼工作呢?分析實際的使用場景,我們可以得到:

  1. 為即将加入記憶體緩存的圖檔生成相應的

  2. 使用 Comparator 判斷鍵是否相等時,可能出現不同的 Uri 生成的鍵相同的情況,此類需要提供方法解決這個問題
  3. 當你輸入一個 Uri 時可能會在記憶體緩存中找到多個響應圖檔,是以方法類應傳回響應圖檔清單
  4. 當你輸入一個 Uri 時可能會在記憶體緩存中找到多個已緩存的鍵,是以方法類應傳回鍵清單
  5. 将對應輸入 Uri 的所有緩存圖檔從記憶體中移除
public static String generateKey(String imageUri, ImageSize targetSize){}

public static Comparator<String> createFuzzyKeyComparator(){}

public static List<Bitmap> findCachedBitmapsForImageUri(String imageUri, MemoryCache memoryCache){}

public static List<String> findCacheKeysForImageUri(String imageUri, MemoryCache memoryCache){}

public static void removeFromCache(String imageUri, MemoryCache memoryCache){}
           

具體實作我就不在這多說拉,大家可以自行檢視源碼進行閱讀,裡面的邏輯并不難。

MemoryCahce 的結構圖

從設計到實作,一步步教你實作Android-Universal-ImageLoader-緩存Cache反思

DiskCache

DiskCache 整體設計及相關基類的實作

實際上 DiskCache 的設計思想和 MemoryCache 是非常接近的,我們隻要遵循剛剛的思路就可以拉。那麼首先需要實作 DiskCache 的抽象:

public interface DiskCache {
    File getDirectory();

    File get(String imageUri);

    boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;

    boolean save(String imageUri, Bitmap bitmap) throws IOException;

    boolean remove(String imageUri);

    void close();

    void clear();
}
           

然後獲得兩個基類:

同樣的,我們根據緩存的限制實作 BaseDiskCache 的細節,就可以完成本地緩存功能子產品拉

DiskCache 的輔助工具類

雖然 DiskCache 整體設計和實作都挺簡單的,但是大家還需要考慮到一個問題,就是我們每一張圖檔緩存到本地之後都需要為其命名,那麼我們就應該為其實作相關的命名類咯。能夠區分每一張圖檔的命名方式無非就是:哈希和MD5,引用網上一張圖,我們應該設計一個檔案命名功能子產品,提供給 DiskCache 使用:

從設計到實作,一步步教你實作Android-Universal-ImageLoader-緩存Cache反思

那麼首先實作命名類的抽象:

public interface FileNameGenerator {
    String generate(String imageUri);
}
           

然後分别實作細節就可以拉:

public class HashCodeFileNameGenerator implements FileNameGenerator {
    @Override
    public String generate(String imageUri) {
        return String.valueOf(imageUri.hashCode());
    }
}
           
public class Md5FileNameGenerator implements FileNameGenerator {

    private static final String HASH_ALGORITHM = "MD5";
    private static final int RADIX =  + ; // 10 digits + 26 letters

    @Override
    public String generate(String imageUri) {
        byte[] md5 = getMD5(imageUri.getBytes());
        BigInteger bi = new BigInteger(md5).abs();
        return bi.toString(RADIX);
    }

    private byte[] getMD5(byte[] data) {
        byte[] hash = null;
        try {
            MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM);
            digest.update(data);
            hash = digest.digest();
        } catch (NoSuchAlgorithmException e) {
            L.e(e);
        }
        return hash;
    }
}
           

DiskCache 的工具類

DiskCache 除了在進行本地緩存時需要工具類幫助其生成檔案名,還需要工具類幫它完成其他工作:本地緩存管理、本地存儲政策等……于是我們需要引入多個工具類協助完成這些職責。

大家需要注意到的是,工具類和輔助工具類将處于不同的包中,因為輔助工具類是完成職責必不可少的一環。而工具類是減少使用者的使用成本,兩者間的差别使得類所在的包不一緻。

DiskCacheUtils

和 MemoryCacheUtils 一樣,DiskCacheUtils 也是一個無法被繼承,無法建立執行個體對象的類。

public final class DiskCacheUtils {

    private DiskCacheUtils() {
    }
}
           

這個工具類的主要使用場景為:

  1. 找到 Uri 對應的本地緩存檔案
  2. 移除 Uri 對應的本地緩存檔案

是以我們分别實作對應的方法就可以拉:

public static File findInCache(String imageUri, DiskCache diskCache){}

public static boolean removeFromCache(String imageUri, DiskCache diskCache){}
           

StorageUtils

為了将圖檔存儲到本地 SD 卡中,我們需要獲得本地緩存對應的目錄,于是引入了 StorageUtils 負責完成相關的事項。由于工具類都是無法繼承和建立對象的類,我就不再放出響應的構造方法和類聲明了。在 StorageUtils 中,我們可能存在的使用場景有:

  1. 獲得本地緩存目錄
  2. 建立額外的本地緩存目錄
  3. 為某些圖檔提供對應的特定緩存目錄
  4. 為單個圖檔提供緩存目錄

實作各自對應的方法,就OK拉。

DiskCahce 的結構圖

從設計到實作,一步步教你實作Android-Universal-ImageLoader-緩存Cache反思

反思

大家會發現,無論是 DiskCache 還是 MemoryCache,還是它們的工具類,代碼結構清晰,類間耦合度低,許多開發者看到這樣的代碼都會感歎:這代碼寫得真漂亮。但我相信觀察力敏銳的同學會一眼看出,MemoryCache 和 DiskCache 都使用了裝飾者模式進行設計,功能的拓展隻要針對對應的抽象進行“裝飾”就可以了;工具類則嚴格遵循設計模式中的設計原則,盡可能獨立不同的工具子產品。是以大家在開發的時候也應該注意這些開發細節,不斷重構代碼,讓代碼結構變得清晰。