轉載請标明出處,本文出自:chaossss的部落格
Android-Universal-ImageLoader Github 位址
Cache
我們要對圖檔進行緩存,有兩種方式:記憶體緩存和本地緩存。這兩種方式的差別在于,記憶體緩存是緩存在 Android 系統為應用配置設定的運作記憶體之中,讀取速度快,但是可能會帶來 OOM 的問題;本地緩存一般緩存在 SD 卡中,讀取速度較慢,但是緩存空間足。
那麼我們要怎麼來實作記憶體緩存和本地緩存呢?根據單一職責原則,如果 MemoryCache 和 DiskCache 的抽象不一緻的話,我們就需要分别建立 MemoryCache 和 DiskCache 的抽象基類,分别實作各自的細節。而顯然,兩者抽象是不一緻的,因為 MemoryCache 面對的對象是圖檔(Bitmap),DiskCache 面對的對象是檔案(File)。是以,我們應該分開實作 MemoryCache 和 DiskCache。
MemoryCache
MemoryCache 整體設計及相關基類的實作
那我們現在該幹啥呢?想 MemoryCache 的功能啊!對于進行記憶體緩存我們能想到什麼應用場景呢:
- 首先每一個 MemoryCache 肯定有相應的增删取功能
- 當 MemoryCache 被存滿了(Android 應用的記憶體資源是很寶貴的),我們該怎麼處理呢:
- 回收最近沒有用過的
- 回收最早在記憶體中緩存的
- 回收使用頻率最低的
- 在緩存時,key 相同的圖檔,回收舊的圖檔,緩存新的圖
- 有時候我們可能需要緩存高分辨率的高清圖檔,而這種圖檔非常大,緩存到記憶體中就會 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 的反射機制的話還是可以建立對象的。
那麼這個類要幫我們完成什麼工作呢?分析實際的使用場景,我們可以得到:
- 為即将加入記憶體緩存的圖檔生成相應的
鍵
- 使用 Comparator 判斷鍵是否相等時,可能出現不同的 Uri 生成的鍵相同的情況,此類需要提供方法解決這個問題
- 當你輸入一個 Uri 時可能會在記憶體緩存中找到多個響應圖檔,是以方法類應傳回響應圖檔清單
- 當你輸入一個 Uri 時可能會在記憶體緩存中找到多個已緩存的鍵,是以方法類應傳回鍵清單
- 将對應輸入 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 的結構圖

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 使用:
那麼首先實作命名類的抽象:
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() {
}
}
這個工具類的主要使用場景為:
- 找到 Uri 對應的本地緩存檔案
- 移除 Uri 對應的本地緩存檔案
是以我們分别實作對應的方法就可以拉:
public static File findInCache(String imageUri, DiskCache diskCache){}
public static boolean removeFromCache(String imageUri, DiskCache diskCache){}
StorageUtils
為了将圖檔存儲到本地 SD 卡中,我們需要獲得本地緩存對應的目錄,于是引入了 StorageUtils 負責完成相關的事項。由于工具類都是無法繼承和建立對象的類,我就不再放出響應的構造方法和類聲明了。在 StorageUtils 中,我們可能存在的使用場景有:
- 獲得本地緩存目錄
- 建立額外的本地緩存目錄
- 為某些圖檔提供對應的特定緩存目錄
- 為單個圖檔提供緩存目錄
實作各自對應的方法,就OK拉。
DiskCahce 的結構圖
反思
大家會發現,無論是 DiskCache 還是 MemoryCache,還是它們的工具類,代碼結構清晰,類間耦合度低,許多開發者看到這樣的代碼都會感歎:這代碼寫得真漂亮。但我相信觀察力敏銳的同學會一眼看出,MemoryCache 和 DiskCache 都使用了裝飾者模式進行設計,功能的拓展隻要針對對應的抽象進行“裝飾”就可以了;工具類則嚴格遵循設計模式中的設計原則,盡可能獨立不同的工具子產品。是以大家在開發的時候也應該注意這些開發細節,不斷重構代碼,讓代碼結構變得清晰。