Android图片加载框架最全解析(三),深入探究Glide的缓存机制

·  阅读 1247

在本系列的上一篇文章中,我带着大家一起阅读了一遍 Glide 的源码,初步了解了这个强大的图片加载框架的基本执行流程。

不过,上一篇文章只能说是比较粗略地阅读了 Glide 整个执行流程方面的源码,搞明白了 Glide 的基本工作原理,但并没有去深入分析每一处的细节(事实上也不可能在一篇文章中深入分析每一处源码的细节)。那么从本篇文章开始,我们就一篇篇地来针对 Glide 某一块功能进行深入地分析,慢慢将 Glide 中的各项功能进行全面掌握。

今天我们就先从缓存这一块内容开始入手吧。不过今天文章中的源码都建在上一篇源码分析的基础之上,还没有看过上一篇文章的朋友,建议先去阅读 Android 图片加载框架最全解析(二),从源码的角度理解 Glide 的执行流程

Glide缓存简介

Glide 的缓存设计可以说是非常先进的,考虑的场景也很周全。在缓存这一功能上,Glide 又将它分成了两个模块,一个是内存缓存,一个是硬盘缓存。

这两个缓存模块的作用各不相同,内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,而硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。

内存缓存和硬盘缓存的相互结合才构成了 Glide 极佳的图片缓存效果,那么接下来我们就分别来分析一下这两种缓存的使用方法以及它们的实现原理。

缓存Key

既然是缓存功能,就必然会有用于进行缓存的 Key。那么 Glide 的缓存 Key 是怎么生成的呢?我不得不说,Glide 的缓存 Key 生成规则非常繁琐,决定缓存 Key 的参数竟然有 10 个之多。不过繁琐归繁琐,至少逻辑还是比较简单的,我们先来看一下 Glide 缓存 Key 的生成逻辑。

生成缓存 Key 的代码在 Engine 类的 load() 方法当中,这部分代码我们在上一篇文章当中已经分析过了,只不过当时忽略了缓存相关的内容,那么我们现在重新来看一下:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
            Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
        Util.assertMainThread();
        long startTime = LogTime.getLogTime();

        final String id = fetcher.getId();
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());

        ...
    }

    ...
}
复制代码

可以看到,这里在第 11 行调用了 fetcher.getId() 方法获得了一个 id 字符串,这个字符串也就是我们要加载的图片的唯一标识,比如说如果是一张网络上的图片的话,那么这个 id 就是这张图片的 url 地址。

接下来在第 12 行,将这个 id 连同着 signature、width、height 等等 10 个参数一起传入到 EngineKeyFactory 的 buildKey() 方法当中,从而构建出了一个 EngineKey 对象,这个 EngineKey 也就是 Glide 中的缓存 Key 了。

可见,决定缓存 Key 的条件非常多,即使你用 override() 方法改变了一下图片的 width 或者 height,也会生成一个完全不同的缓存 Key。

EngineKey 类的源码大家有兴趣可以自己去看一下,其实主要就是重写了 equals() 和 hashCode() 方法,保证只有传入 EngineKey 的所有参数都相同的情况下才认为是同一个 EngineKey 对象,我就不在这里将源码贴出来了。

内存缓存

有了缓存 Key,接下来就可以开始进行缓存了,那么我们先从内存缓存看起。

首先你要知道,默认情况下,Glide 自动就是开启内存缓存的。也就是说,当我们使用 Glide 加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用 Glide 再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了,这样无疑就可以大幅度提升图片的加载效率。比方说你在一个 RecyclerView 当中反复上下滑动,RecyclerView 中只要是 Glide 加载过的图片都可以直接从内存当中迅速读取并展示出来,从而大大提升了用户体验。

而 Glide 最为人性化的是,你甚至不需要编写任何额外的代码就能自动享受到这个极为便利的内存缓存功能,因为 Glide 默认就已经将它开启了。

那么既然已经默认开启了这个功能,还有什么可讲的用法呢?只有一点,如果你有什么特殊的原因需要禁用内存缓存功能,Glide 对此提供了接口:

Glide.with(this)
     .load(url)
     .skipMemoryCache(true)
     .into(imageView);
复制代码

可以看到,只需要调用 skipMemoryCache() 方法并传入 true,就表示禁用掉 Glide 的内存缓存功能。

没错,关于 Glide 内存缓存的用法就只有这么多,可以说是相当简单。但是我们不可能只停留在这么简单的层面上,接下来就让我们就通过阅读源码来分析一下 Glide 的内存缓存功能是如何实现的。

其实说到内存缓存的实现,非常容易就让人想到 LruCache 算法(Least Recently Used),也叫近期最少使用算法。它的主要算法原理就是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。LruCache 的用法也比较简单,我在 Android 高效加载大图、多图解决方案,有效避免程序 OOM 这篇文章当中有提到过它的用法,感兴趣的朋友可以去参考一下。

那么不必多说,Glide 内存缓存的实现自然也是使用的 LruCache 算法。不过除了 LruCache 算法之外,Glide 还结合了一种弱引用的机制,共同完成了内存缓存功能,下面就让我们来通过源码分析一下。

首先回忆一下,在上一篇文章的第二步 load() 方法中,我们当时分析到了在 loadGeneric() 方法中会调用 Glide.buildStreamModelLoader() 方法来获取一个 ModelLoader 对象。当时没有再跟进到这个方法的里面再去分析,那么我们现在来看下它的源码:

public class Glide {

    public static <T, Y> ModelLoader<T, Y> buildModelLoader(Class<T> modelClass, Class<Y> resourceClass,
            Context context) {
         if (modelClass == null) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Unable to load null model, setting placeholder only");
            }
            return null;
        }
        return Glide.get(context).getLoaderFactory().buildModelLoader(modelClass, resourceClass);
    }

    public static Glide get(Context context) {
        if (glide == null) {
            synchronized (Glide.class) {
                if (glide == null) {
                    Context applicationContext = context.getApplicationContext();
                    List<GlideModule> modules = new ManifestParser(applicationContext).parse();
                    GlideBuilder builder = new GlideBuilder(applicationContext);
                    for (GlideModule module : modules) {
                        module.applyOptions(applicationContext, builder);
                    }
                    glide = builder.createGlide();
                    for (GlideModule module : modules) {
                        module.registerComponents(applicationContext, glide);
                    }
                }
            }
        }
        return glide;
    }

    ...
}
复制代码

这里我们还是只看关键,在第 11 行去构建 ModelLoader 对象的时候,先调用了一个 Glide.get() 方法,而这个方法就是关键。我们可以看到,get() 方法中实现的是一个单例功能,而创建 Glide 对象则是在第 24 行调用 GlideBuilder 的 createGlide() 方法来创建的,那么我们跟到这个方法当中:

public class GlideBuilder {
    ...

    Glide createGlide() {
        if (sourceService == null) {
            final int cores = Math.max(1, Runtime.getRuntime().availableProcessors());
            sourceService = new FifoPriorityThreadPoolExecutor(cores);
        }
        if (diskCacheService == null) {
            diskCacheService = new FifoPriorityThreadPoolExecutor(1);
        }
        MemorySizeCalculator calculator = new MemorySizeCalculator(context);
        if (bitmapPool == null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                int size = calculator.getBitmapPoolSize();
                bitmapPool = new LruBitmapPool(size);
            } else {
                bitmapPool = new BitmapPoolAdapter();
            }
        }
        if (memoryCache == null) {
            memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
        }
        if (diskCacheFactory == null) {
            diskCacheFactory = new InternalCacheDiskCacheFactory(context);
        }
        if (engine == null) {
            engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
        }
        if (decodeFormat == null) {
            decodeFormat = DecodeFormat.DEFAULT;
        }
        return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
    }
}
复制代码

这里也就是构建 Glide 对象的地方了。那么观察第 22 行,你会发现这里 new 出了一个 LruResourceCache,并把它赋值到了 memoryCache 这个对象上面。你没有猜错,这个就是 Glide 实现内存缓存所使用的 LruCache 对象了。不过我这里并不打算展开来讲 LruCache 算法的具体实现,如果你感兴趣的话可以自己研究一下它的源码。

现在创建好了 LruResourceCache 对象只能说是把准备工作做好了,接下来我们就一步步研究 Glide 中的内存缓存到底是如何实现的。

刚才在 Engine 的 load() 方法中我们已经看到了生成缓存 Key 的代码,而内存缓存的代码其实也是在这里实现的,那么我们重新来看一下 Engine 类 load() 方法的完整源码:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {
    ...    

    public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
            Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
        Util.assertMainThread();
        long startTime = LogTime.getLogTime();

        final String id = fetcher.getId();
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());

        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
            cb.onResourceReady(cached);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from cache", startTime, key);
            }
            return null;
        }

        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
            cb.onResourceReady(active);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from active resources", startTime, key);
            }
            return null;
        }

        EngineJob current = jobs.get(key);
        if (current != null) {
            current.addCallback(cb);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Added to existing load", startTime, key);
            }
            return new LoadStatus(cb, current);
        }

        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority);
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable);

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
    }

    ...
}
复制代码

可以看到,这里在第 17 行调用了 loadFromCache() 方法来获取缓存图片,如果获取到就直接调用 cb.onResourceReady() 方法进行回调。如果没有获取到,则会在第 26 行调用 loadFromActiveResources() 方法来获取缓存图片,获取到的话也直接进行回调。只有在两个方法都没有获取到缓存的情况下,才会继续向下执行,从而开启线程来加载图片。

也就是说,Glide 的图片加载过程中会调用两个方法来获取内存缓存,loadFromCache() 和 loadFromActiveResources()。这两个方法中一个使用的就是 LruCache 算法,另一个使用的就是弱引用。我们来看一下它们的源码:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    private final MemoryCache cache;
    private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
    ...

    private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
            return null;
        }
        EngineResource<?> cached = getEngineResourceFromCache(key);
        if (cached != null) {
            cached.acquire();
            activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
        }
        return cached;
    }

    private EngineResource<?> getEngineResourceFromCache(Key key) {
        Resource<?> cached = cache.remove(key);
        final EngineResource result;
        if (cached == null) {
            result = null;
        } else if (cached instanceof EngineResource) {
            result = (EngineResource) cached;
        } else {
            result = new EngineResource(cached, true );
        }
        return result;
    }

    private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
            return null;
        }
        EngineResource<?> active = null;
        WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
        if (activeRef != null) {
            active = activeRef.get();
            if (active != null) {
                active.acquire();
            } else {
                activeResources.remove(key);
            }
        }
        return active;
    }

    ...
}
复制代码

在 loadFromCache() 方法的一开始,首先就判断了 isMemoryCacheable 是不是 false,如果是 false 的话就直接返回 null。这是什么意思呢?其实很简单,我们刚刚不是学了一个 skipMemoryCache() 方法吗?如果在这个方法中传入 true,那么这里的 isMemoryCacheable 就会是 false,表示内存缓存已被禁用。

我们继续住下看,接着调用了 getEngineResourceFromCache() 方法来获取缓存。在这个方法中,会使用缓存 Key 来从 cache 当中取值,而这里的 cache 对象就是在构建 Glide 对象时创建的 LruResourceCache,那么说明这里其实使用的就是 LruCache 算法了。

但是呢,观察第 22 行,当我们从 LruResourceCache 中获取到缓存图片之后会将它从缓存中移除,然后在第 16 行将这个缓存图片存储到 activeResources 当中。activeResources 就是一个弱引用的 HashMap,用来缓存正在使用中的图片,我们可以看到,loadFromActiveResources() 方法就是从 activeResources 这个 HashMap 当中取值的。使用 activeResources 来缓存正在使用中的图片,可以保护这些图片不会被 LruCache 算法回收掉。

好的,从内存缓存中读取数据的逻辑大概就是这些了。概括一下来说,就是如果能从内存缓存当中读取到要加载的图片,那么就直接进行回调,如果读取不到的话,才会开启线程执行后面的图片加载逻辑。

现在我们已经搞明白了内存缓存读取的原理,接下来的问题就是内存缓存是在哪里写入的呢?这里我们又要回顾一下上一篇文章中的内容了。还记不记得我们之前分析过,当图片加载完成之后,会在 EngineJob 当中通过 Handler 发送一条消息将执行逻辑切回到主线程当中,从而执行 handleResultOnMainThread() 方法。那么我们现在重新来看一下这个方法,代码如下所示:

class EngineJob implements EngineRunnable.EngineRunnableManager {

    private final EngineResourceFactory engineResourceFactory;
    ...

    private void handleResultOnMainThread() {
        if (isCancelled) {
            resource.recycle();
            return;
        } else if (cbs.isEmpty()) {
            throw new IllegalStateException("Received a resource without any callbacks to notify");
        }
        engineResource = engineResourceFactory.build(resource, isCacheable);
        hasResource = true;
        engineResource.acquire();
        listener.onEngineJobComplete(key, engineResource);
        for (ResourceCallback cb : cbs) {
            if (!isInIgnoredCallbacks(cb)) {
                engineResource.acquire();
                cb.onResourceReady(engineResource);
            }
        }
        engineResource.release();
    }

    static class EngineResourceFactory {
        public <R> EngineResource<R> build(Resource<R> resource, boolean isMemoryCacheable) {
            return new EngineResource<R>(resource, isMemoryCacheable);
        }
    }
    ...
}
复制代码

在第 13 行,这里通过 EngineResourceFactory 构建出了一个包含图片资源的 EngineResource 对象,然后会在第 16 行将这个对象回调到 Engine 的 onEngineJobComplete() 方法当中,如下所示:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {
    ...    

    @Override
    public void onEngineJobComplete(Key key, EngineResource<?> resource) {
        Util.assertMainThread();
        
        if (resource != null) {
            resource.setResourceListener(key, this);
            if (resource.isCacheable()) {
                activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
            }
        }
        jobs.remove(key);
    }

    ...
}
复制代码

现在就非常明显了,可以看到,在第 13 行,回调过来的 EngineResource 被 put 到了 activeResources 当中,也就是在这里写入的缓存。

那么这只是弱引用缓存,还有另外一种 LruCache 缓存是在哪里写入的呢?这就要介绍一下 EngineResource 中的一个引用机制了。观察刚才的 handleResultOnMainThread() 方法,在第 15 行和第 19 行有调用 EngineResource 的 acquire() 方法,在第 23 行有调用它的 release() 方法。其实,EngineResource 是用一个 acquired 变量用来记录图片被引用的次数,调用 acquire() 方法会让变量加 1,调用 release() 方法会让变量减 1,代码如下所示:

class EngineResource<Z> implements Resource<Z> {

    private int acquired;
    ...

    void acquire() {
        if (isRecycled) {
            throw new IllegalStateException("Cannot acquire a recycled resource");
        }
        if (!Looper.getMainLooper().equals(Looper.myLooper())) {
            throw new IllegalThreadStateException("Must call acquire on the main thread");
        }
        ++acquired;
    }

    void release() {
        if (acquired <= 0) {
            throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
        }
        if (!Looper.getMainLooper().equals(Looper.myLooper())) {
            throw new IllegalThreadStateException("Must call release on the main thread");
        }
        if (--acquired == 0) {
            listener.onResourceReleased(key, this);
        }
    }
}
复制代码

也就是说,当 acquired 变量大于 0 的时候,说明图片正在使用中,也就应该放到 activeResources 弱引用缓存当中。而经过 release() 之后,如果 acquired 变量等于 0 了,说明图片已经不再被使用了,那么此时会在第 24 行调用 listener 的 onResourceReleased() 方法来释放资源,这个 listener 就是 Engine 对象,我们来看下它的 onResourceReleased() 方法:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    private final MemoryCache cache;
    private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
    ...    

    @Override
    public void onResourceReleased(Key cacheKey, EngineResource resource) {
        Util.assertMainThread();
        activeResources.remove(cacheKey);
        if (resource.isCacheable()) {
            cache.put(cacheKey, resource);
        } else {
            resourceRecycler.recycle(resource);
        }
    }

    ...
}
复制代码

可以看到,这里首先会将缓存图片从 activeResources 中移除,然后再将它 put 到 LruResourceCache 当中。这样也就实现了正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用 LruCache 来进行缓存的功能。

这就是 Glide 内存缓存的实现原理。

硬盘缓存

接下来我们开始学习硬盘缓存方面的内容。

不知道你还记不记得,在本系列的第一篇文章中我们就使用过硬盘缓存的功能了。当时为了禁止 Glide 对图片进行硬盘缓存而使用了如下代码:

Glide.with(this)
     .load(url)
     .diskCacheStrategy(DiskCacheStrategy.NONE)
     .into(imageView);
复制代码

调用 diskCacheStrategy() 方法并传入 DiskCacheStrategy.NONE,就可以禁用掉 Glide 的硬盘缓存功能了。

这个 diskCacheStrategy() 方法基本上就是 Glide 硬盘缓存功能的一切,它可以接收四种参数:

  • DiskCacheStrategy.NONE: 表示不缓存任何内容。
  • DiskCacheStrategy.SOURCE: 表示只缓存原始图片。
  • DiskCacheStrategy.RESULT: 表示只缓存转换过后的图片(默认选项)。
  • DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。

上面四种参数的解释本身并没有什么难理解的地方,但是有一个概念大家需要了解,就是当我们使用 Glide 去加载一张图片的时候,Glide 默认并不会将原始图片展示出来,而是会对图片进行压缩和转换(我们会在后面学习这方面的内容)。总之就是经过种种一系列操作之后得到的图片,就叫转换过后的图片。而 Glide 默认情况下在硬盘缓存的就是转换过后的图片,我们通过调用 diskCacheStrategy() 方法则可以改变这一默认行为。

好的,关于 Glide 硬盘缓存的用法也就只有这么多,那么接下来还是老套路,我们通过阅读源码来分析一下,Glide 的硬盘缓存功能是如何实现的。

首先,和内存缓存类似,硬盘缓存的实现也是使用的 LruCache 算法,而且 Google 还提供了一个现成的工具类 DiskLruCache。我之前也专门写过一篇文章对这个 DiskLruCache 工具进行了比较全面的分析,感兴趣的朋友可以参考一下 Android DiskLruCache 完全解析,硬盘缓存的最佳方案 。当然,Glide 是使用的自己编写的 DiskLruCache 工具类,但是基本的实现原理都是差不多的。

接下来我们看一下 Glide 是在哪里读取硬盘缓存的。这里又需要回忆一下上篇文章中的内容了,Glide 开启线程来加载图片后会执行 EngineRunnable 的 run() 方法,run() 方法中又会调用一个 decode() 方法,那么我们重新再来看一下这个 decode() 方法的源码:

private Resource<?> decode() throws Exception {
    if (isDecodingFromCache()) {
        return decodeFromCache();
    } else {
        return decodeFromSource();
    }
}
复制代码

可以看到,这里会分为两种情况,一种是调用 decodeFromCache() 方法从硬盘缓存当中读取图片,一种是调用 decodeFromSource() 来读取原始图片。默认情况下 Glide 会优先从缓存当中读取,只有缓存中不存在要读取的图片时,才会去读取原始图片。那么我们现在来看一下 decodeFromCache() 方法的源码,如下所示:

private Resource<?> decodeFromCache() throws Exception {
    Resource<?> result = null;
    try {
        result = decodeJob.decodeResultFromCache();
    } catch (Exception e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Exception decoding result from cache: " + e);
        }
    }
    if (result == null) {
        result = decodeJob.decodeSourceFromCache();
    }
    return result;
}
复制代码

可以看到,这里会先去调用 DecodeJob 的 decodeResultFromCache() 方法来获取缓存,如果获取不到,会再调用 decodeSourceFromCache() 方法获取缓存,这两个方法的区别其实就是 DiskCacheStrategy.RESULT 和 DiskCacheStrategy.SOURCE 这两个参数的区别,相信不需要我再做什么解释吧。

那么我们来看一下这两个方法的源码吧,如下所示:

public Resource<Z> decodeResultFromCache() throws Exception {
    if (!diskCacheStrategy.cacheResult()) {
        return null;
    }
    long startTime = LogTime.getLogTime();
    Resource<T> transformed = loadFromCache(resultKey);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    return result;
}

public Resource<Z> decodeSourceFromCache() throws Exception {
    if (!diskCacheStrategy.cacheSource()) {
        return null;
    }
    long startTime = LogTime.getLogTime();
    Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
    return transformEncodeAndTranscode(decoded);
}
复制代码

可以看到,它们都是调用了 loadFromCache() 方法从缓存当中读取数据,如果是 decodeResultFromCache() 方法就直接将数据解码并返回,如果是 decodeSourceFromCache() 方法,还要调用一下 transformEncodeAndTranscode() 方法先将数据转换一下再解码并返回。

然而我们注意到,这两个方法中在调用 loadFromCache() 方法时传入的参数却不一样,一个传入的是 resultKey,另外一个却又调用了 resultKey 的 getOriginalKey() 方法。这个其实非常好理解,刚才我们已经解释过了,Glide 的缓存 Key 是由 10 个参数共同组成的,包括图片的 width、height 等等。但如果我们是缓存的原始图片,其实并不需要这么多的参数,因为不用对图片做任何的变化。那么我们来看一下 getOriginalKey() 方法的源码:

public Key getOriginalKey() {
    if (originalKey == null) {
        originalKey = new OriginalKey(id, signature);
    }
    return originalKey;
}
复制代码

可以看到,这里其实就是忽略了绝大部分的参数,只使用了 id 和 signature 这两个参数来构成缓存 Key。而 signature 参数绝大多数情况下都是用不到的,因此基本上可以说就是由 id(也就是图片 url)来决定的 Original 缓存 Key。

搞明白了这两种缓存 Key 的区别,那么接下来我们看一下 loadFromCache() 方法的源码吧:

private Resource<T> loadFromCache(Key key) throws IOException {
    File cacheFile = diskCacheProvider.getDiskCache().get(key);
    if (cacheFile == null) {
        return null;
    }
    Resource<T> result = null;
    try {
        result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
    } finally {
        if (result == null) {
            diskCacheProvider.getDiskCache().delete(key);
        }
    }
    return result;
}
复制代码

这个方法的逻辑非常简单,调用 getDiskCache() 方法获取到的就是 Glide 自己编写的 DiskLruCache 工具类的实例,然后调用它的 get() 方法并把缓存 Key 传入,就能得到硬盘缓存的文件了。如果文件为空就返回 null,如果文件不为空则将它解码成 Resource 对象后返回即可。

这样我们就将硬盘缓存读取的源码分析完了,那么硬盘缓存又是在哪里写入的呢?趁热打铁我们赶快继续分析下去。

刚才已经分析过了,在没有缓存的情况下,会调用 decodeFromSource() 方法来读取原始图片。那么我们来看下这个方法:

public Resource<Z> decodeFromSource() throws Exception {
    Resource<T> decoded = decodeSource();
    return transformEncodeAndTranscode(decoded);
}
复制代码

这个方法中只有两行代码,decodeSource() 顾名思义是用来解析原图片的,而 transformEncodeAndTranscode() 则是用来对图片进行转换和转码的。我们先来看 decodeSource() 方法:

private Resource<T> decodeSource() throws Exception {
    Resource<T> decoded = null;
    try {
        long startTime = LogTime.getLogTime();
        final A data = fetcher.loadData(priority);
        if (isCancelled) {
            return null;
        }
        decoded = decodeFromSourceData(data);
    } finally {
        fetcher.cleanup();
    }
    return decoded;
}

private Resource<T> decodeFromSourceData(A data) throws IOException {
    final Resource<T> decoded;
    if (diskCacheStrategy.cacheSource()) {
        decoded = cacheAndDecodeSourceData(data);
    } else {
        long startTime = LogTime.getLogTime();
        decoded = loadProvider.getSourceDecoder().decode(data, width, height);
    }
    return decoded;
}

private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
    long startTime = LogTime.getLogTime();
    SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
    diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
    startTime = LogTime.getLogTime();
    Resource<T> result = loadFromCache(resultKey.getOriginalKey());
    return result;
}
复制代码

这里会在第 5 行先调用 fetcher 的 loadData() 方法读取图片数据,然后在第 9 行调用 decodeFromSourceData() 方法来对图片进行解码。接下来会在第 18 行先判断是否允许缓存原始图片,如果允许的话又会调用 cacheAndDecodeSourceData() 方法。而在这个方法中同样调用了 getDiskCache() 方法来获取 DiskLruCache 实例,接着调用它的 put() 方法就可以写入硬盘缓存了,注意原始图片的缓存 Key 是用的 resultKey.getOriginalKey()。

好的,原始图片的缓存写入就是这么简单,接下来我们分析一下 transformEncodeAndTranscode() 方法的源码,来看看转换过后的图片缓存是怎么写入的。代码如下所示:

private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
    long startTime = LogTime.getLogTime();
    Resource<T> transformed = transform(decoded);
    writeTransformedToCache(transformed);
    startTime = LogTime.getLogTime();
    Resource<Z> result = transcode(transformed);
    return result;
}

private void writeTransformedToCache(Resource<T> transformed) {
    if (transformed == null || !diskCacheStrategy.cacheResult()) {
        return;
    }
    long startTime = LogTime.getLogTime();
    SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
    diskCacheProvider.getDiskCache().put(resultKey, writer);
}
复制代码

这里的逻辑就更加简单明了了。先是在第 3 行调用 transform() 方法来对图片进行转换,然后在 writeTransformedToCache() 方法中将转换过后的图片写入到硬盘缓存中,调用的同样是 DiskLruCache 实例的 put() 方法,不过这里用的缓存 Key 是 resultKey。

这样我们就将 Glide 硬盘缓存的实现原理也分析完了。虽然这些源码看上去如此的复杂,但是经过 Glide 出色的封装,使得我们只需要通过 skipMemoryCache() 和 diskCacheStrategy() 这两个方法就可以轻松自如地控制 Glide 的缓存功能了。

了解了 Glide 缓存的实现原理之后,接下来我们再来学习一些 Glide 缓存的高级技巧吧。

虽说 Glide 将缓存功能高度封装之后,使得用法变得非常简单,但同时也带来了一些问题。

比如之前有一位群里的朋友就跟我说过,他们项目的图片资源都是存放在七牛云上面的,而七牛云为了对图片资源进行保护,会在图片 url 地址的基础之上再加上一个 token 参数。也就是说,一张图片的 url 地址可能会是如下格式:

http://url.com/image.jpg?token=d9caa6e02c990b0a
复制代码

而使用 Glide 加载这张图片的话,也就会使用这个 url 地址来组成缓存 Key。

但是接下来问题就来了,token 作为一个验证身份的参数并不是一成不变的,很有可能时时刻刻都在变化。而如果 token 变了,那么图片的 url 也就跟着变了,图片 url 变了,缓存 Key 也就跟着变了。结果就造成了,明明是同一张图片,就因为 token 不断在改变,导致 Glide 的缓存功能完全失效了。

这其实是个挺棘手的问题,而且我相信绝对不仅仅是七牛云这一个个例,大家在使用 Glide 的时候很有可能都会遇到这个问题。

那么该如何解决这个问题呢?我们还是从源码的层面进行分析,首先再来看一下 Glide 生成缓存 Key 这部分的代码:

public class Engine implements EngineJobListener,
        MemoryCache.ResourceRemovedListener,
        EngineResource.ResourceListener {

    public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
            Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
        Util.assertMainThread();
        long startTime = LogTime.getLogTime();

        final String id = fetcher.getId();
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());

        ...
    }

    ...
}
复制代码

来看一下第 11 行,刚才已经说过了,这个 id 其实就是图片的 url 地址。那么,这里是通过调用 fetcher.getId() 方法来获取的图片 url 地址,而我们在上一篇文章中已经知道了,fetcher 就是 HttpUrlFetcher 的实例,我们就来看一下它的 getId() 方法的源码吧,如下所示:

public class HttpUrlFetcher implements DataFetcher<InputStream> {

    private final GlideUrl glideUrl;
    ...

    public HttpUrlFetcher(GlideUrl glideUrl) {
        this(glideUrl, DEFAULT_CONNECTION_FACTORY);
    }

    HttpUrlFetcher(GlideUrl glideUrl, HttpUrlConnectionFactory connectionFactory) {
        this.glideUrl = glideUrl;
        this.connectionFactory = connectionFactory;
    }

    @Override
    public String getId() {
        return glideUrl.getCacheKey();
    }

    ...
}
复制代码

可以看到,getId() 方法中又调用了 GlideUrl 的 getCacheKey() 方法。那么这个 GlideUrl 对象是从哪里来的呢?其实就是我们在 load() 方法中传入的图片 url 地址,然后 Glide 在内部把这个 url 地址包装成了一个 GlideUrl 对象。

很明显,接下来我们就要看一下 GlideUrl 的 getCacheKey() 方法的源码了,如下所示:

public class GlideUrl {

    private final URL url;
    private final String stringUrl;
    ...

    public GlideUrl(URL url) {
        this(url, Headers.DEFAULT);
    }

    public GlideUrl(String url) {
        this(url, Headers.DEFAULT);
    }

    public GlideUrl(URL url, Headers headers) {
        ...
        this.url = url;
        stringUrl = null;
    }

    public GlideUrl(String url, Headers headers) {
        ...
        this.stringUrl = url;
        this.url = null;
    }

    public String getCacheKey() {
        return stringUrl != null ? stringUrl : url.toString();
    }

    ...
}

复制代码

这里我将代码稍微进行了一点简化,这样看上去更加简单明了。GlideUrl 类的构造函数接收两种类型的参数,一种是 url 字符串,一种是 URL 对象。然后 getCacheKey() 方法中的判断逻辑非常简单,如果传入的是 url 字符串,那么就直接返回这个字符串本身,如果传入的是 URL 对象,那么就返回这个对象 toString() 后的结果。

其实看到这里,我相信大家已经猜到解决方案了,因为 getCacheKey() 方法中的逻辑太直白了,直接就是将图片的 url 地址进行返回来作为缓存 Key 的。那么其实我们只需要重写这个 getCacheKey() 方法,加入一些自己的逻辑判断,就能轻松解决掉刚才的问题了。

创建一个 MyGlideUrl 继承自 GlideUrl,代码如下所示:

public class MyGlideUrl extends GlideUrl {

    private String mUrl;

    public MyGlideUrl(String url) {
        super(url);
        mUrl = url;
    }

    @Override
    public String getCacheKey() {
        return mUrl.replace(findTokenParam(), "");
    }

    private String findTokenParam() {
        String tokenParam = "";
        int tokenKeyIndex = mUrl.indexOf("?token=") >= 0 ? mUrl.indexOf("?token=") : mUrl.indexOf("&token=");
        if (tokenKeyIndex != -1) {
            int nextAndIndex = mUrl.indexOf("&", tokenKeyIndex + 1);
            if (nextAndIndex != -1) {
                tokenParam = mUrl.substring(tokenKeyIndex + 1, nextAndIndex + 1);
            } else {
                tokenParam = mUrl.substring(tokenKeyIndex);
            }
        }
        return tokenParam;
    }

}
复制代码

可以看到,这里我们重写了 getCacheKey() 方法,在里面加入了一段逻辑用于将图片 url 地址中 token 参数的这一部分移除掉。这样 getCacheKey() 方法得到的就是一个没有 token 参数的 url 地址,从而不管 token 怎么变化,最终 Glide 的缓存 Key 都是固定不变的了。

当然,定义好了 MyGlideUrl,我们还得使用它才行,将加载图片的代码改成如下方式即可:

Glide.with(this)
     .load(new MyGlideUrl(url))
     .into(imageView);
复制代码

也就是说,我们需要在 load() 方法中传入这个自定义的 MyGlideUrl 对象,而不能再像之前那样直接传入 url 字符串了。不然的话 Glide 在内部还是会使用原始的 GlideUrl 类,而不是我们自定义的 MyGlideUrl 类。

这样我们就将这个棘手的缓存问题给解决掉了。

好了,关于 Glide 缓存方面的内容今天就分析到这里,现在我们不光掌握了 Glide 缓存的基本用法和高级技巧,还了解了它背后的实现原理,又是收获满满的一篇文章啊。下一篇文章当中,我会继续带着大家深入分析 Glide 的其他功能模块,讲一讲回调方面的知识,感兴趣的朋友请继续阅读 Android 图片加载框架最全解析(四),玩转 Glide 的回调与监听

关注我的技术公众号“郭霖”,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改