Glide源码修改-自定义磁盘缓存实现永久存储

Glide图片磁盘缓存

在《Glide实现WebView离线图片的酷炫展示效果》一文中,在webview中的图片我们通过Glide缓存起来,并且将html的内容保存到文件,最终可以在离线下实现文章阅读。其中的图片资源通过glide缓存到cache目录下,我们知道,Glide在加载网络图片时可以将图片缓存到内存与磁盘中。当我们下次使用时,就可以从缓存中获取显示图片。其中的磁盘缓存使用的是DiskLruCache算法。
 但是缓存文件总大小是有限制的,当超过250M(默认)时,会根据LRU算法删除一些久未使用的图片资源。但是这样会导致离线情况下无法加载一些WebView中的图片(如果)。因此我们希望的是将如果html页面被离线保存下来,那么其中的图片也把它放到一个永久保存的目录下,这些图片资源不会记录到LRU中。

Glide磁盘缓存过程源码分析

 为了能够实现上面的图片永久存储效果,就需要自定义磁盘缓存策略。在这之前,我们需要大致了解下Glide的源码,它是如何将图片缓存到磁盘,又如何使用磁盘中的图片资源的。

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

通过AS调试into方法,发现最终会调用RequestManagertrack方法,request实际类型为SingleRequest

glide-RequestBuilder-into.png

进入track方法,发现执行的是SingleRequestbegin方法

glide-RequestTracker-runRequest.png

跟踪调试SingleRequestbegin方法, 执行了DrawableImageViewTargetgetSize方法,并且把自己作为回调参数传入,类型为SizeReadyCallback,猜测估计最终调用onSizeReady方法。

glide-SingleRequest-begin.png

通过AS快捷键搜索(mac使用command+o)DrawableImageViewTarget类,然后搜索getSize方法(command+F12,输入getSize),找到对应的实现类ViewTarget

glide-DrawableImageViewTarget-getSize.png

继续调试ViewTarget方法,可以发现是通过ViewTreeObserver获取ImageView获取大小,然后最终回调SingleRequestonSizeReady方法。

glide-viewTarget-getSize.png

回到SingleRequestonSizeReady方法。status置为RUNNING状态。然后执行Engine中的load方法,继续跟进,发现执行了DecodeJobrunWrapped方法。

glide-DecodeJob-runWrapped-INITALIZE.png

runGenerators方法中,循环执行了currentGenerator.startNext()方法。
DataFetcherGenerator接口的实现类有三个

  • ResourceCacheGenerator
  • DataCacheGenerator
  • SourceGenerator
    而在SourceGeneratorstartNext会执行cacheToData缓存磁盘操作,通过DiskLruCacheWrapperput方法将图片资源缓存到磁盘。
    glide-DiskLruCacheWrapper-put.png

最终的磁盘缓存算法是通过DiskLruCache类来实现。

DiskLruCache

Glide通过DiskLruCache将图片的原始资源,以及一些指定执行尺寸的资源缓存至磁盘。主要包含一个名为journal的索引文件,以及多个经过hash命名后的图片资源文件。
一个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版本,应用版本(),每个缓存项的数量(valueCount)。
第6行开始表示了不同状态的缓存记录,每条记录相关信息通过空格隔开:

状态+key+指定状态的相关属性
  • DIRTY表示数据正在创建或者更新操作,每个DIRTY脏操作之后都应该有一个CLEAN或者REMOVE操作,没有匹配的CLEANREMOVE的脏行表示可能需要删除临时文件。
  • CLEAN表示已成功发布并可能被读取的缓存项,后面表示的是每个值的长度,如果valueCount为2,有两个文件key.0key.1。后面两个数值表示文件长度。
  • READ表示图片的读取记录,它会进入LRU。
  • REMOVE表示缓存资源的删除操作。

默认的DiskLruCache缓存的最大值为250M,当图片文件总长度操作这一数值是,就会进行REMOVE操作。

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

如果我们离线缓存的图片需要永久使用,那就不能把那些离线图片计入LRU中,不然,当有一天,缓存文件超出250M,一些离线图片就会被删除。因此我们需要定义一个新的PERMANENT(永久)状态,不会进入LRU,并且,我们将这些图片放入另外的文件夹下面。

自定义DiskLruCache支持永久存储

首先我们需要使得Glide支持自定义DiskCache,使用@GlideModule定义一个AppGlideModule,然后在applyOptions方法中实现缓存自定义。

@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字段,使得它支持图片永久存储操作。

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";//长久文件(不进入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;
            }
            //永久区
            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);
        }
        ...
    }
    /**
     * 读取永久区的文件
     *
     * @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;
    }
    /**
     * 将缓存的文件移动到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文件放入永久区
     */
    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;
    }
}

在通过Glide实现WebView离线图片展示时,将图片资源永久存储有两种情况,一种是图片已经加载,磁盘中已经有相关资源,我们可以通过cacheToPermanent方法,将图片从缓存目录移动到永久目录。另外一种是图片未加载,这种情况离线缓存的话需要下载图片,然后tempToPermanent直接放入永久区域。

永久图片Glide存储

项目地址

https://github.com/iamyours/Wandroid

你可能感兴趣的:(Glide源码修改-自定义磁盘缓存实现永久存储)