Glide源碼修改-自定義磁盤緩存實(shí)現(xiàn)永久存儲(chǔ)

Glide圖片磁盤緩存

《Glide實(shí)現(xiàn)WebView離線圖片的酷炫展示效果》一文中昧谊,在webview中的圖片我們通過(guò)Glide緩存起來(lái),并且將html的內(nèi)容保存到文件,最終可以在離線下實(shí)現(xiàn)文章閱讀血公。其中的圖片資源通過(guò)glide緩存到cache目錄下隐解,我們知道邪媳,Glide在加載網(wǎng)絡(luò)圖片時(shí)可以將圖片緩存到內(nèi)存與磁盤中。當(dāng)我們下次使用時(shí)叽唱,就可以從緩存中獲取顯示圖片。其中的磁盤緩存使用的是DiskLruCache算法微宝。
?但是緩存文件總大小是有限制的棺亭,當(dāng)超過(guò)250M(默認(rèn))時(shí),會(huì)根據(jù)LRU算法刪除一些久未使用的圖片資源蟋软。但是這樣會(huì)導(dǎo)致離線情況下無(wú)法加載一些WebView中的圖片(如果)镶摘。因此我們希望的是將如果html頁(yè)面被離線保存下來(lái),那么其中的圖片也把它放到一個(gè)永久保存的目錄下岳守,這些圖片資源不會(huì)記錄到LRU中凄敢。

Glide磁盤緩存過(guò)程源碼分析

?為了能夠?qū)崿F(xiàn)上面的圖片永久存儲(chǔ)效果,就需要自定義磁盤緩存策略湿痢。在這之前涝缝,我們需要大致了解下Glide的源碼,它是如何將圖片緩存到磁盤譬重,又如何使用磁盤中的圖片資源的拒逮。

Glide.with(this).load(url).into(image)

通過(guò)AS調(diào)試into方法,發(fā)現(xiàn)最終會(huì)調(diào)用RequestManagertrack方法,request實(shí)際類型為SingleRequest

glide-RequestBuilder-into.png

進(jìn)入track方法臀规,發(fā)現(xiàn)執(zhí)行的是SingleRequestbegin方法

glide-RequestTracker-runRequest.png

跟蹤調(diào)試SingleRequestbegin方法, 執(zhí)行了DrawableImageViewTargetgetSize方法滩援,并且把自己作為回調(diào)參數(shù)傳入,類型為SizeReadyCallback,猜測(cè)估計(jì)最終調(diào)用onSizeReady方法塔嬉。

glide-SingleRequest-begin.png

通過(guò)AS快捷鍵搜索(mac使用command+o)DrawableImageViewTarget類玩徊,然后搜索getSize方法(command+F12,輸入getSize)租悄,找到對(duì)應(yīng)的實(shí)現(xiàn)類ViewTarget

glide-DrawableImageViewTarget-getSize.png

繼續(xù)調(diào)試ViewTarget方法佣赖,可以發(fā)現(xiàn)是通過(guò)ViewTreeObserver獲取ImageView獲取大小恰矩,然后最終回調(diào)SingleRequestonSizeReady方法。

glide-viewTarget-getSize.png

回到SingleRequestonSizeReady方法憎蛤。status置為RUNNING狀態(tài)外傅。然后執(zhí)行Engine中的load方法,繼續(xù)跟進(jìn)俩檬,發(fā)現(xiàn)執(zhí)行了DecodeJobrunWrapped方法萎胰。

glide-DecodeJob-runWrapped-INITALIZE.png

runGenerators方法中,循環(huán)執(zhí)行了currentGenerator.startNext()方法棚辽。
DataFetcherGenerator接口的實(shí)現(xiàn)類有三個(gè)

  • ResourceCacheGenerator
  • DataCacheGenerator
  • SourceGenerator
    而在SourceGeneratorstartNext會(huì)執(zhí)行cacheToData緩存磁盤操作,通過(guò)DiskLruCacheWrapperput方法將圖片資源緩存到磁盤技竟。
    glide-DiskLruCacheWrapper-put.png

最終的磁盤緩存算法是通過(guò)DiskLruCache類來(lái)實(shí)現(xiàn)。

DiskLruCache

Glide通過(guò)DiskLruCache將圖片的原始資源屈藐,以及一些指定執(zhí)行尺寸的資源緩存至磁盤榔组。主要包含一個(gè)名為journal的索引文件,以及多個(gè)經(jīng)過(guò)hash命名后的圖片資源文件联逻。
一個(gè)journal文件可能是這樣的:

libcore.io.DiskLruCache
1
100
2

CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

前4行分別表示文件頭信息搓扯,disk cache版本,應(yīng)用版本()包归,每個(gè)緩存項(xiàng)的數(shù)量(valueCount)锨推。
第6行開始表示了不同狀態(tài)的緩存記錄,每條記錄相關(guān)信息通過(guò)空格隔開:

狀態(tài)+key+指定狀態(tài)的相關(guān)屬性
  • DIRTY表示數(shù)據(jù)正在創(chuàng)建或者更新操作公壤,每個(gè)DIRTY臟操作之后都應(yīng)該有一個(gè)CLEAN或者REMOVE操作换可,沒有匹配的CLEANREMOVE的臟行表示可能需要?jiǎng)h除臨時(shí)文件。
  • CLEAN表示已成功發(fā)布并可能被讀取的緩存項(xiàng)厦幅,后面表示的是每個(gè)值的長(zhǎng)度沾鳄,如果valueCount為2,有兩個(gè)文件key.0key.1确憨。后面兩個(gè)數(shù)值表示文件長(zhǎng)度洞渔。
  • READ表示圖片的讀取記錄,它會(huì)進(jìn)入LRU缚态。
  • REMOVE表示緩存資源的刪除操作磁椒。

默認(rèn)的DiskLruCache緩存的最大值為250M,當(dāng)圖片文件總長(zhǎng)度操作這一數(shù)值是玫芦,就會(huì)進(jìn)行REMOVE操作浆熔。

//DiskLruCache.java
private void trimToSize() throws IOException {
    while (size > maxSize) {
      Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
      remove(toEvict.getKey());
    }
}

如果我們離線緩存的圖片需要永久使用,那就不能把那些離線圖片計(jì)入LRU中,不然医增,當(dāng)有一天慎皱,緩存文件超出250M,一些離線圖片就會(huì)被刪除叶骨。因此我們需要定義一個(gè)新的PERMANENT(永久)狀態(tài)茫多,不會(huì)進(jìn)入LRU,并且忽刽,我們將這些圖片放入另外的文件夾下面天揖。

自定義DiskLruCache支持永久存儲(chǔ)

首先我們需要使得Glide支持自定義DiskCache,使用@GlideModule定義一個(gè)AppGlideModule跪帝,然后在applyOptions方法中實(shí)現(xiàn)緩存自定義今膊。

@GlideModule
public class MyAppGlideModule extends AppGlideModule {
    
    @Override
    public void applyOptions(@NonNull Context context,
                             @NonNull GlideBuilder builder) {
        super.applyOptions(context, builder);
        builder.setDiskCache(new WanDiskCacheFactory(new WanDiskCacheFactory.CacheDirectoryGetter() {
            @NotNull
            @Override
            public File getCacheDirectory() {
                return new File(context.getCacheDir(), "wandroid_images");
            }

            @NotNull
            @Override
            public File getPermanentDirectory() {
                return new File(context.getFilesDir(), "permanent_images");
            }
        }));
    }
}

修改DiskCacheFactory源碼,改為WanDiskCacheFactory,新增permanentDirectory目錄

class WanDiskCacheFactory(var cacheDirectoryGetter: CacheDirectoryGetter) :
    DiskCache.Factory {

    interface CacheDirectoryGetter {
        val cacheDirectory: File
        val permanentDirectory: File
    }

    override fun build(): DiskCache? {
        val cacheDir: File =
            cacheDirectoryGetter.cacheDirectory
        val permanentDirectory = cacheDirectoryGetter.permanentDirectory
        cacheDir.mkdirs()
        permanentDirectory.mkdirs()

        return if ((!cacheDir.exists()
                    || !cacheDir.isDirectory
                    || !permanentDirectory.exists()
                    || !permanentDirectory.isDirectory)
        ) {
            null
        } else WanDiskLruCacheWrapper.create(
            permanentDirectory,
            cacheDir,
            20 * 1024 * 1024//262144000L(250M) for cache
        )

    }
}

修改DiskLruCacheWanDiskLruCache伞剑,添加PERMANENT字段斑唬,使得它支持圖片永久存儲(chǔ)操作。

public final class WanDiskLruCache implements Closeable {
    static final String MAGIC = "libcore.io.WanDiskLruCache";
    ...
    private static final String CLEAN = "CLEAN";
    private static final String DIRTY = "DIRTY";
    private static final String REMOVE = "REMOVE";
    private static final String READ = "READ";
    private static final String PERMANENT = "PERMANENT";//長(zhǎng)久文件(不進(jìn)入LRU)
    private void readJournalLine(String line) throws IOException {
        int firstSpace = line.indexOf(' ');
        if (firstSpace == -1) {
            throw new IOException("unexpected journal line: " + line);
        }

        int keyBegin = firstSpace + 1;
        int secondSpace = line.indexOf(' ', keyBegin);
        final String key;
        if (secondSpace == -1) {
            key = line.substring(keyBegin);
            if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
                lruEntries.remove(key);
                permanentEntries.remove(key);
                return;
            }
            //永久區(qū)
            if (firstSpace == PERMANENT.length() && line.startsWith(PERMANENT)) {
                lruEntries.remove(key);
                Entry entry = permanentEntries.get(key);
                if (entry == null) {
                    entry = new Entry(key, permanentDirectory);
                    entry.readable = true;
                    permanentEntries.put(key, entry);
                }
                return;
            }
        } else {
            key = line.substring(keyBegin, secondSpace);
        }

        ...
    }
    public synchronized Value get(String key) throws IOException {
        checkNotClosed();
        Entry permanentEntry = readPermanentEntry(key);
        if (permanentEntry != null) {
            StringKt.logV("read from permanent permanent directory:" + key);
            return new Value(key, permanentEntry.sequenceNumber,
                    permanentEntry.cleanFiles,
                    permanentEntry.lengths);
        }
        ...
    }
    /**
     * 讀取永久區(qū)的文件
     *
     * @param key
     * @return
     */
    private Entry readPermanentEntry(String key) throws IOException {
        Entry entry = permanentEntries.get(key);
        if (entry == null) {
            entry = new Entry(key, permanentDirectory);
            entry.readable = true;
            for (File file : entry.cleanFiles) {
                // A file must have been deleted manually!
                if (!file.exists()) {
                    return null;
                }
            }
            addOpt(PERMANENT, key);
        }
        return entry;
    }
    /**
     * 將緩存的文件移動(dòng)到permanent下
     */
    public synchronized boolean cacheToPermanent(String key) throws IOException {
        checkNotClosed();
        Entry entry = lruEntries.get(key);
        if (entry == null || entry.currentEditor != null) {
            StringKt.logV("cacheToPermanent null:" + key);
            return false;
        }

        for (int i = 0; i < valueCount; i++) {
            File file = entry.getCleanFile(i);
            if (file.exists()) {
                FileUtil.copyFileToDirectory(file, permanentDirectory);
                file.delete();
            }
            size -= entry.lengths[i];
            StringKt.logV("cacheToPermanent:" + entry.getLengths() + ",key:" + entry.key);
            entry.lengths[i] = 0;
        }
        Entry pEntry = new Entry(key, permanentDirectory);
        pEntry.readable = true;
        permanentEntries.put(key, pEntry);
        lruEntries.remove(key);
        addOpt(PERMANENT, key);

        return true;
    }

    /**
     * 刪除永久圖片
     *
     * @param key
     * @return
     * @throws IOException
     */
    public synchronized boolean removePermanent(String key) throws IOException {
        checkNotClosed();
        Entry entry = readPermanentEntry(key);
        if (entry == null) return false;
        for (int i = 0; i < valueCount; i++) {
            File file = entry.getCleanFile(i);
            if (file.exists() && !file.delete()) {
                throw new IOException("failed to delete " + file);
            }
            permanentEntries.remove(key);
        }
        addOpt(REMOVE, key);
        return true;
    }

    /**
     * 將下載好的tmp文件放入永久區(qū)
     */
    public synchronized boolean tempToPermanent(File tmp, String key) throws IOException {
        StringKt.logV("tempToPermanent:" + key);
        Entry entry = new Entry(key, permanentDirectory);
        for (int i = 0; i < valueCount; i++) {
            File file = entry.getCleanFile(i);
            tmp.renameTo(file);
        }
        permanentEntries.put(key, entry);
        addOpt(PERMANENT, key);
        return true;
    }
}

在通過(guò)Glide實(shí)現(xiàn)WebView離線圖片展示時(shí)黎泣,將圖片資源永久存儲(chǔ)有兩種情況恕刘,一種是圖片已經(jīng)加載,磁盤中已經(jīng)有相關(guān)資源抒倚,我們可以通過(guò)cacheToPermanent方法褐着,將圖片從緩存目錄移動(dòng)到永久目錄。另外一種是圖片未加載衡便,這種情況離線緩存的話需要下載圖片献起,然后tempToPermanent直接放入永久區(qū)域洋访。

永久圖片Glide存儲(chǔ)

項(xiàng)目地址

https://github.com/iamyours/Wandroid

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末镣陕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子姻政,更是在濱河造成了極大的恐慌呆抑,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汁展,死亡現(xiàn)場(chǎng)離奇詭異鹊碍,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)食绿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門侈咕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人器紧,你說(shuō)我怎么就攤上這事耀销。” “怎么了铲汪?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵熊尉,是天一觀的道長(zhǎng)罐柳。 經(jīng)常有香客問(wèn)我,道長(zhǎng)狰住,這世上最難降的妖魔是什么张吉? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮催植,結(jié)果婚禮上肮蛹,老公的妹妹穿的比我還像新娘。我一直安慰自己查邢,他們只是感情好蔗崎,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扰藕,像睡著了一般缓苛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上邓深,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天未桥,我揣著相機(jī)與錄音,去河邊找鬼芥备。 笑死冬耿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的萌壳。 我是一名探鬼主播亦镶,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼袱瓮!你這毒婦竟也來(lái)了缤骨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤尺借,失蹤者是張志新(化名)和其女友劉穎绊起,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體燎斩,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡虱歪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了栅表。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笋鄙。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖怪瓶,靈堂內(nèi)的尸體忽然破棺而出萧落,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布铐尚,位于F島的核電站拨脉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏宣增。R本人自食惡果不足惜玫膀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望爹脾。 院中可真熱鬧帖旨,春花似錦、人聲如沸灵妨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)泌霍。三九已至货抄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間朱转,已是汗流浹背蟹地。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留藤为,地道東北人怪与。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像缅疟,于是被迫代替她去往敵國(guó)和親分别。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容