Imageloader源碼解析-從頭教你如何開始看“硬盤緩存策略”源碼

從基本配置中尋找代碼的入口:

這篇文章是ImageLoader的硬盤緩存策略完成解析,寫起來比較詳細,所以字數和圖片都比較多,希望大家能認識讀完。


Paste_Image.png

一般情況下,我們都要配置上圖的Imageloer配置,明顯可以看出來,這里用到了“建造者模式”來完成基本數據參數的注入,我們都知道,Imageloader是通過三級訪問,來實現對圖片文件的展示的,
三級分別是:網絡(Net)、硬盤(Disk)、內存(Memory)。這次我寫的文章不是總結性的文章,而是先帶大家,如何一步一步去看源代碼,之后再進行總結。

硬盤緩存策略的配置入口ImageLoaderConfiguration.Builder. diskCache(disck) :

首先我們來了解一下,默認情況下,硬盤緩存會使用哪種策略:

Paste_Image.png

點擊 ctrl+鼠標選中 ,能夠跳轉到 ImageLoaderConfiguration類 這個類的職責是完成ImageLodaer的配置 diskCache(disck) 是其建造類的方法,注入了硬盤緩存算法:

Paste_Image.png

通過注釋我們可以看出來,這個函數方法的作用就是設置圖片硬盤緩存策略的,默認使用的的硬盤策略是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目錄 如下:

Paste_Image.png

以下是我看過類后畫出的UML類圖:

Paste_Image.png

我這里來解釋下上面的圖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是左邊鍵盤的不能按數字鍵),你會看到:

Paste_Image.png

你能看到有一個Builder類,這是建造者模式的標配,再看下來,我要找的是createDefault(Context context)這個方法函數進入過程如下:

createDefault(Context context)-->.build()-->initEmptyFieldsWithDefaultValues()(這個函數就是初始化默認值和空值)-->

Paste_Image.png

看圖:這里是硬盤緩存為空時的核心,顯判斷diskCacheFileNameGenerator是否為空,不為空就創建默認的,這里的diskCacheFileNameGenerator是硬盤的名字生成器,因為硬盤緩存策略都要命名,所以這里傳入的名字生成器不能為null 下來就是重點了,我們繼續點.createDiskCache進去,我們可以看到,我們調到這個新的類型里面去,這個類叫DefaultConfigurationFactory,其只要功能就是生成一些默認的配置,我們不管其他,繼續看我們之前那方法,createDiskCache()意思就是說創建一個硬盤緩存策略:

Paste_Image.png

看注釋,我們就懂:創建一個取決于傳入參數的繼承于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流的工具類吧:

Paste_Image.png

上面就是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():

Paste_Image.png

我們能看到,保存文件的方法,還是使用父類的,但是這里他擴展了一個方法,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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容

  • title: imageLoader解析date: 2017-09-02 19:00:47categories: ...
    小人物灌籃閱讀 1,961評論 0 7
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,711評論 18 399
  • 10.MemoryCache MemoryCache是實現內存緩存的類,不管是內存緩存還是磁盤緩存,對于Image...
    反復橫跳的龍套閱讀 502評論 1 0
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,615評論 25 708
  • 東方降瑞見云梯, 赤縣氤氳幻彩霓。 朗朗乾坤祥紫繞, 中華崛起獻瑰麗。
    劉豫州閱讀 456評論 5 7