美文网首页美文共赏
Glide源码修改-自定义磁盘缓存实现永久存储

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

作者: 星星y | 来源:发表于2020-11-10 14:24 被阅读0次

    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<String, 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源码修改-自定义磁盘缓存实现永久存储

        本文链接:https://www.haomeiwen.com/subject/hcbkmktx.html