從基本配置中尋找代碼的入口:
這篇文章是ImageLoader的硬盤緩存策略完成解析,寫起來比較詳細,所以字數和圖片都比較多,希望大家能認識讀完。
一般情況下,我們都要配置上圖的Imageloer配置,明顯可以看出來,這里用到了“建造者模式”來完成基本數據參數的注入,我們都知道,Imageloader是通過三級訪問,來實現對圖片文件的展示的,
三級分別是:網絡(Net)、硬盤(Disk)、內存(Memory)。這次我寫的文章不是總結性的文章,而是先帶大家,如何一步一步去看源代碼,之后再進行總結。
硬盤緩存策略的配置入口ImageLoaderConfiguration.Builder. diskCache(disck) :
首先我們來了解一下,默認情況下,硬盤緩存會使用哪種策略:
點擊 ctrl+鼠標選中 ,能夠跳轉到 ImageLoaderConfiguration類 這個類的職責是完成ImageLodaer的配置 diskCache(disck) 是其建造類的方法,注入了硬盤緩存算法:
通過注釋我們可以看出來,這個函數方法的作用就是設置圖片硬盤緩存策略的,默認使用的的硬盤策略是UnlimitedDiskCache這個緩存策略,默認的路徑通過StorageUtils.getCacheDirectory(Context)來獲取到,我們暫時不考慮他們內部的實現過程,我們只需要知道,默認情況下,我們什么都不設置的時候回使用UnlimitedDiskCache這種緩存策略,和默認的cache路徑從StorageUtils.getCacheDirectory(Context)獲取。
探尋硬盤緩存策略ImageLoader實現多少種策略:
接下來我們會先考慮一個問題,我們如何知道,Imageloade中到底有多少種緩存策略呢。。根據java建立包的規范,我們能考慮到,硬盤緩存的所有實現方法應該是放在同一目錄下的,所以我們先看看UnlimitedDiskCache所在的目錄“com.nostra13.universalimageloader.cache.disc.impl”,建包規范或者了熟悉面對對象編程的朋友通過這個目錄,可以知道,這個目錄下應該就是硬盤緩存的具體實現,(我使用的是Android studio 可以直接打開jar包) 我們直接點開 Universal-ImageLoader.jar包 的com.nostra13.universalimageloader.cache.disc目錄 如下:
以下是我看過類后畫出的UML類圖:
我這里來解釋下上面的圖DiskCache是一個接口,通過“實現關系”規范BaseDisckCache和LruDiskCache必須實現的方法,BaseDiskCache是一個抽象類,實現的就是最簡單的保存網絡圖片文件到硬盤,獲取本地硬盤,并且沒有任何限制,LruDiskCache類實現DiskCache,是基于“最近最少使用”算法來實現的,而LimitedAgeDiskCache和UnlimitedDiskCache跟BaseDiskCache有很多共同點,相對之下,LimitedAgeDiskCache只是對時間進行控制,對超時的圖片文件進行刪除處理,而UnlimitedDiskCache沒有任何限制,他們對緩存的大小都沒有控制,而LruDiskCache會控制緩存大小和緩存的文件多少,所以他們都繼承BaseDiskCache抽象類。
我們來總結一下,Imageloder中的硬盤緩存有三種策略:
LruDiskCache:最近最少使用緩存策略 考慮文件緩存大小和緩存文件多少
LimitedAgeDiskCache:設置文件存活時間,當文件超過這個時間時就刪除該文件,不考慮文件緩存大小
UnlimitedDiskCache:沒有任何限制的存取策略,不考慮文件緩存大小
上面是硬盤緩存的三種策略,通過分析我們知道,LruDiskCache是里面非常好的策略,所以我們設置的時候盡量設置LruDiskCache,因為默認情況下我們使用的是UnlimitedDiskCache,這樣對于用戶來說是非常不友好的,因為每個用戶的手機配置是不相同的,有些存儲比較少的時候,這樣就能給用戶更加友好的體驗,安卓的發展就靠大家了。。。
為什么默認策略是UnlimitedDiskCache:
掌握了大致的方向之后,我們可以思考一個問題:為什么默認的情況下是UnlimitedDiskCache,在代碼中,Imageloder框架的如何實現的呢。。。
默認配置,也是屬于配置,所以我們看一下ImageLoaderConfiguration類,在Android studio 中進入ImageLoaderConfiguration類 ,鍵盤設置ctrl+7(這個7是左邊鍵盤的不能按數字鍵),你會看到:
你能看到有一個Builder類,這是建造者模式的標配,再看下來,我要找的是createDefault(Context context)這個方法函數進入過程如下:
createDefault(Context context)-->.build()-->initEmptyFieldsWithDefaultValues()(這個函數就是初始化默認值和空值)-->
看圖:這里是硬盤緩存為空時的核心,顯判斷diskCacheFileNameGenerator是否為空,不為空就創建默認的,這里的diskCacheFileNameGenerator是硬盤的名字生成器,因為硬盤緩存策略都要命名,所以這里傳入的名字生成器不能為null 下來就是重點了,我們繼續點.createDiskCache進去,我們可以看到,我們調到這個新的類型里面去,這個類叫DefaultConfigurationFactory,其只要功能就是生成一些默認的配置,我們不管其他,繼續看我們之前那方法,createDiskCache()意思就是說創建一個硬盤緩存策略:
看注釋,我們就懂:創建一個取決于傳入參數的繼承于DiskCache的類,這句話怎么理解呢?意思就是說創建一個繼承于DiskCache的類,但是具體實現的策略由傳入的參數的決定,我們看86-95的判斷就知道,影響的參數是diskCacheSize和diskCacheFileCount,如果有其中一個值大于0,我們使用的策略就是LruDiskCache,所以當我們需要使用LruDiskCache硬盤緩存策略的就只需要設置其中一個值為正整數就行了,當我們什么都不設置的時候,默認就會執行96-97行,使用的就是UnlimitedDiskCache。
下面說一下整個createDiskCache思路:
85行是:創建磁盤緩存文件夾,如果主磁盤緩存文件夾不可用,將使用該磁盤緩存文件夾,也就是一個備份區。
86行:對diskCacheSize或者diskCacheFileCount進行判斷,如果大于0,執行87行 然后返回一個LruDiskCache實現對象。
87行:獲取到一個保存圖片的私人文件夾。
89-90行:返回LruDiskCache實現對象。
96-97行:獲取獲取文件夾路徑,并返回沒有任何限制的實現硬盤緩存。
探尋硬盤緩存策略ImageLoader三種策略的具體實現:
DiskCache:
之前已經說了三種策略都實現于DiskCache,接下來我們看看其源代碼:
public interface DiskCache {
/**
硬盤緩存的接口
*/
/**
返回硬盤緩存的保存文件夾
*/
File getDirectory();
/**
* 獲取到硬盤緩存的圖片文件
*
* @param 圖片唯一url
* @return File of cached image or <b>null</b> if image wasn't cached
*/
File get(String imageUri);
/**
* 在磁盤緩存中保存圖像流。
* 此方法不應關閉傳入的圖像流
*
*/
boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;
/**
* 保存磁盤緩存中的圖像位圖。
*
*/
boolean save(String imageUri, Bitmap bitmap) throws IOException;
/**
* 刪除輸入URI關聯的圖像文件
*/
boolean remove(String imageUri);
/** Closes disk cache, releases resources. */
void close();
/** Clears disk cache. */
void clear();
}
這個接口就是規定硬盤緩存策略必須實現的方法,具體方法實現由具體類來實現,在這里使用接口是為了遵循面向對象的六大原則中的開閉原則,和里式替換原則。
BaseDiskCache:
下面,我們來看看BaseDiskCache的抽象類,我們主要看核心的三個方法:
1.boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener):在磁盤緩存中保存圖像流
2.boolean save(String imageUri, Bitmap bitmap):保存磁盤緩存中的圖像位圖
3.File get(String imageUri):獲取到硬盤緩存的圖片文件
我們一個個方法來看看 BaseDiskCache是如何實現的:
save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener)
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
//根據url返回一個文件
File imageFile = getFile(imageUri);
//生成一個以.tmp結尾的臨時文件
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
//聲明是否保存成功
boolean loaded = false;
try {
//創建一個緩沖輸入流
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
try {
//調用IoUtils的.copyStream復制流的函數方法讀寫到輸入流中
loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
} finally {
//關閉臨時文件
IoUtils.closeSilently(os);
}
} finally {
//結束后加載成功并轉移到imageFile不成功時 loaded設置為false
if (loaded && !tmpFile.renameTo(imageFile)) {
loaded = false;
}
//loaded為false,刪除臨時文件
if (!loaded) {
tmpFile.delete();
}
}
return loaded;
}
上面就是直接對圖像流進行保存,注釋寫的很多,我就不一一說了,下面我們看一下getFile(imageUri)的實現:
//通過imgeurl返回一個非空的文件,文件可以引用一個不存在的文件。
protected File getFile(String imageUri) {
//根據文件名成生成器生成文件名 這個文件名生成器有 兩種實現方式,后面我們再說
String fileName = fileNameGenerator.generate(imageUri);
File dir = cacheDir;
//如果滿足cacheDir不存在并且!cacheDir.mkdirs()說明磁盤不可操作,我就使用備用的文件夾
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
dir = reserveCacheDir;
}
}
return new File(dir, fileName);
}
這里應該很好理解,就是獲取cache文件的根目錄,來生成一個以fileName命名的文件。
下面我們來看一下IoUtils這個Io流的工具類吧:
上面就是IoUtIls類的結構,主要的職能是復制圖片文件,停止加載文件,當我們調用copyStream方法有三、四個參數兩種多態,最終都會調用四個參數的,只是三個參數的不設置一次讀取緩存流的大小,默認為DEFAULT_BUFFER_SIZE = 32 * 1024,下來我們來看看四個參數的:
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
//根據url返回一個文件
File imageFile = getFile(imageUri);
//生成一個以.tmp結尾的臨時文件
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
//聲明是否保存成功
boolean loaded = false;
try {
//創建一個緩沖輸入流
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
try {
//調用IoUtils的.copyStream復制流的函數方法讀寫到輸入流中
loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
} finally {
//關閉臨時文件
IoUtils.closeSilently(os);
}
} finally {
//結束后加載成功并轉移到imageFile不成功時 loaded設置為false
if (loaded && !tmpFile.renameTo(imageFile)) {
loaded = false;
}
//loaded為false,刪除臨時文件
if (!loaded) {
tmpFile.delete();
}
}
return loaded;
}
//通過imgeurl返回一個非空的文件,文件可以引用一個不存在的文件。
protected File getFile(String imageUri) {
//根據文件名成生成器生成文件名 這個文件名生成器有 兩種實現方式,后面我們再說
String fileName = fileNameGenerator.generate(imageUri);
File dir = cacheDir;
//如果滿足cacheDir不存在并且!cacheDir.mkdirs()說明磁盤不可操作,我就使用備用的文件夾
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
dir = reserveCacheDir;
}
}
return new File(dir, fileName);
}
下面直接看一下IOUtils類:
/**
* 復制流、通過監聽器發進度監聽,監聽器可以中斷復制過程
*
* @param is 輸入流
* @param os 輸出流
* @param listener 復制過程和可以中斷復制的監聽器
* @param bufferSize 用于復制的緩沖流大小
* @return 如果為true 復制完成 如果falae中斷復制
* @throws IOException
*/
public static boolean copyStream(InputStream is, OutputStream os, CopyListener listener, int bufferSize)
throws IOException {
int current = 0;
//獲取輸入流的總大小
int total = is.available();
//如果為負數 就設置為圖片默認的總大小為500 * 1024 即為500kb
if (total <= 0) {
total = DEFAULT_IMAGE_TOTAL_SIZE;
}
//創建字節數組
final byte[] bytes = new byte[bufferSize];
int count;
//首先判斷是否應該停止正在復制的流 返回true 中斷復制
if (shouldStopLoading(listener, current, total)) return false;
//遍歷開始復制
while ((count = is.read(bytes, 0, bufferSize)) != -1) {
//寫入
os.write(bytes, 0, count);
//記錄當前復制完成的大小
current += count;
//每次都判斷一下
if (shouldStopLoading(listener, current, total)) return false;
}
//刷新輸出流
os.flush();
return true;
}
//是否應該停止正在復制的任務
private static boolean shouldStopLoading(CopyListener listener, int current, int total) {
//如果中斷監聽器為空直接返回false不中斷
if (listener != null) {
//是否中斷看客戶端具體實現監聽方法
boolean shouldContinue = listener.onBytesCopied(current, total);
//如果判斷為中斷后 ,還需要判斷 是否加載少于75% 我們才會返回true中斷
if (!shouldContinue) {
if (100 * current / total < CONTINUE_LOADING_PERCENTAGE) {
return true; // if loaded more than 75% then continue loading anyway
}
}
}
return false;
}
//關閉流
public static void closeSilently(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception ignored) {
}
}
}
//讀取 并關閉流
public static void readAndCloseStream(InputStream is) {
final byte[] bytes = new byte[DEFAULT_BUFFER_SIZE];
try {
while (is.read(bytes, 0, DEFAULT_BUFFER_SIZE) != -1);
} catch (IOException ignored) {
} finally {
closeSilently(is);
}
}
public boolean save(String imageUri, Bitmap bitmap)
下面我們講一下保存磁盤緩存中的圖像位圖怎么處理的:
public boolean save(String imageUri, Bitmap bitmap) throws IOException {
File imageFile = getFile(imageUri);
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
//是否保存成功
boolean savedSuccessfully = false;
try {
//直接調用Bitmap的復制方法
savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
} finally {
IoUtils.closeSilently(os);
//如果成功 但是沒有轉移成功 判斷為保存失敗
if (savedSuccessfully && !tmpFile.renameTo(imageFile)) {
savedSuccessfully = false;
}
//保存失敗刪除臨時文件
if (!savedSuccessfully) {
tmpFile.delete();
}
}
//釋放Bitmap 因為btmap是非常耗費內存的
bitmap.recycle();
return savedSuccessfully;
}
比之前的簡單,獲取圖片文件更加簡單,我這里就不寫了。
繼承于BaseDiskCache的LimitedAgeDiskCache和UnlimitedDiskCache和其父類有何不同呢?
我們先說UnlimitedDiskCache,這個方法是沒有任何限制,所以他直接繼承了BaseDiskCache然后啥米事情都沒干。
那么LimitedAgeDiskCache是怎么限制時間的呢?
我們看到源碼中擴展了兩個變量:
private final long maxFileAge;
private final Map<File, Long> loadingDates = Collections.synchronizedMap(new HashMap<File, Long>());
我們暫時不考慮這兩個變量有啥用,我們關注重點的那兩個方法,先看save():
我們能看到,保存文件的方法,還是使用父類的,但是這里他擴展了一個方法,rememberUsage(imageUri),這也是常用的擴展方法的方法,rememberUsage(imageUri)具體實現為:
private void rememberUsage(String imageUri) {
File file = getFile(imageUri);
long currentTime = System.currentTimeMillis();
file.setLastModified(currentTime);
loadingDates.put(file, currentTime);
}
這個方法就是實現,獲取文件,然后設置當前時間為最后修改的時間,存儲到強引用loadingDates變量中,現在我們大概能猜出,loadingDates的作用了吧,就是為了以文件為key,更改時間為values來記錄,文件的保存時間。
下面我們看看:public File get(String imageUri)
public File get(String imageUri) {
//從父類獲取文件 一模一樣
File file = super.get(imageUri);
//如果file不為空,并且存在
if (file != null && file.exists()) {
//是否緩存有時間保存緩存
boolean cached;
//直接從loadingDates獲取
Long loadingDate = loadingDates.get(file);
//沒有緩存記錄存在
if (loadingDate == null) {
//設置cached為false 說明之前沒有訪問過
cached = false;
//loadingDate從文件中獲取最后更改時間
loadingDate = file.lastModified();
} else {
//有訪問過緩存
cached = true;
}
//判斷當前時間是否過時
if (System.currentTimeMillis() - loadingDate > maxFileAge) {
//過時就刪除文件
file.delete();
//刪除緩存記錄
loadingDates.remove(file);
} else if (!cached) {
//沒有過時 又沒有本地緩存有訪問記錄 就添加
loadingDates.put(file, loadingDate);
}
}
return file;
}
上面不管是save還有get方法,我們發現都是采用新生成一個子類,重寫部分方法來實現功能的擴展。當然我們也可以通過裝飾者模式來實現功能的拓展。這是我發現的兩張擴展比較好的方法。
DisKLrucache作為一個重點,另外篇幅講解:http://www.lxweimin.com/p/d03f10b18dff