Glide强大的缓存功能模块

前言

磁盘缓存和内存缓存

一般图片加载框架的缓存模块设计都会有磁盘缓存内存缓存这两级缓存。

磁盘缓存的目的是为了在显示一张图片资源时不需要每次去从网络上重新请求,当第一次从网络上获取到图片资源时保存在本地以便下次显示的时候直接从本地磁盘中获取。

内存缓存的目的是为了在显示一张图片资源时不需要每次进行io操作从本地磁盘上获取,当第一次从磁盘上获取到图片资源时保存在内存中以便下次显示的时候直接从内存中获取加快加载速度。

缓存的缺点是时效性较低,但这在图片缓存中并不存在,图片一般以一个url来标识唯一性,图片资源发生变化时通常会有一个新的url生成,所以可以说缓存模块在图片加载框架中是很有必要的。

但仅有磁盘缓存和内存缓存不够

Bitmap缓存复用

移动设备中对内存的使用管理要求很高,而图片资源往往是比较占内存的,这就要求我们在加载图片时尽量合理的使用内存,虽然有了图片内存缓存程序在一般情况都都能工作得很好,不会出现大的问题,但是内存缓存的容量总是有限度的(一般为LRU策略),不能无限制地增长,所以程序还是会周期性的申请和释放内存。特别是在一个长列表快速滑动时,会造成大量的图片申请和释放,导致 GC 频繁,进而产生卡顿

所以这就要求我们对图片显示实体对象有一个复用策略,避免大量的图片申请和释放

Android 中图片的显示实体是 Bitmap 对象,每次图片显示时会将图片资源构建成一个 Bitmap 对象,创建 Bitmap 和销毁 Bitmap 是比较消耗系统资源的,所以能让 Bitmap 可复用可有效降低 Bitmap的创建和销毁和频繁的内存申请和释放操作,从而减少卡顿提高应用性能,在3.0以前 Bitmap 的数据是存在 native 区域,3.0以后存在 Dalvik 内存区域,API11 后 系统提供了 Bitmap 复用的 API
Managing Bitmap Memory

Glide 中为了复用 Bitmap 构建了一个 BitmapPool,图片的 Bitmap 的申请和释放都需要通过它来处理。需要加载新的图片时,先从 BitmapPool 中查找有没有相应大小或者稍大一点的 Bitmap,有则直接使用,没有再创建新的 Bitmap。一个长列表中的图片往往是大小相同的,所以这个复用率还是相当可观的。

另外 Glide中还有一个 activeResource 缓存,存放的是正在使用的图片资源对象,数据结构为 HashMap<Key, WeakReference<EngineResource<?>>> 不同于内存缓存用 Lru 算法策略的是,该缓存是无容量大小限制的,内部用引用计数来确定是否被外界所正在使用,当引用为0时会从该缓存中移除,加入到内存缓存或者 BitmapPool 中。该缓存的作用是保护正在使用的资源并复用,试想一下如果没有这级缓存,只有 LruMemoryCache 那么当 LruMemoryCache 缓存达到容量上限时有可能移除掉正在使用的图片资源,当应用中另外一个地方需要同时显示同样的图片资源时Glide将在内存中找不到这个资源对象则又会重建一个新的资源对象。

Glide中这几种缓存除了 activeResource 缓存外都使用了LruCache策略,关于LruCache策略可以回看我之前发的文章
http://ethanhua.cn/archives/181

功能需求分析完了,剩下的就是具体的代码实现了,我将会从图片的加载流程中摘取出有关于缓存模块的流程分析

为了避免直接扎入源码分析中迷失自我,先简单明了来一张流程总结图,通过这张流程图就能窥伺Glide的缓存模块的主体实现策略了。

四种缓存数据结构之间的数据流动图

缓存流程分析

读取内存缓存

Glide.with().load().into()into方法里构建了request 并执行了它的 begin方法,begin 方法中会在确定了 target 的宽和高后执行 onSizeReady 回调转到 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) {
        ......

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

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

        /// 从activeResources中获取
        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
            ///GenericRequest
            cb.onResourceReady(active);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from active resources", startTime, key);
            }
            return null;
        }
        //从磁盘和网络中获取
        ......
    }

    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;
    }

    ///从LruResourceCache中获取,若有则移除并放入activesource
    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;
    }

    @SuppressWarnings("unchecked")
    private EngineResource<?> getEngineResourceFromCache(Key key) {
        Resource<?> cached = cache.remove(key);

        final EngineResource result;
        if (cached == null) {
            result = null;
        } else if (cached instanceof EngineResource) {
            // Save an object allocation if we've cached an EngineResource (the typical case).
            result = (EngineResource) cached;
        } else {
            result = new EngineResource(cached, true /*isCacheable*/);
        }
        return result;
    }
}

先调用 loadFromCache() 从 MemoryCache 中获取,如果命中则将缓存从 MemoryCache 中移除并放入 activeResources,然后返回。如果缓存失效则尝试从 activeResources 中获取,如果都失效再构建 EngineRunnable 从磁盘或者网络获取。

是否决定从 MemoryCache 和 activeResources 中获取的前提条件是 isMemoryCacheable 为 true。这个值从 GenericRequestBuilder 传过来,默认为true,也就是说 Glide 默认开启内存缓存。除非你主动调用了 skipMemoryCache() 使该加载请求跳过内存缓存。该方法就是通过将 isMemoryCacheable 置为 false 实现的。

这里出现了 MemoryCache 和 activeResources,它们一起构成的 Glide 中的内存缓存。它们的 Key,都是由 url、图片大小、decoder、encoder等变量组成。MemoryCache 用于保存最近使用过而当前不在使用的 EngineResource,在 GlideBuilder 中被初始化。activeResources 用于保存当前正在被使用的 EngineResource,是一个使用 Key 作为键, EngineResource 的弱引用为值的 HashMap

读取磁盘缓存

当 MemoryCache 和 activeResources 都失效时,程序才构建一个 EngineRunnable 并交给线程池执行。在 EngineRunnable 的 run() 方法中就调用了 decode() 尝试从磁盘或网络获取图片,在decode() 方法里面会先调用 decodeFromCache() 从磁盘中获取图片,如果没有则调用 decodeFromSource() 从源端获取。 从磁盘中获取图片流程中 先会调用 decodeResultFromCache() 获取处理图 如果没有则调用decodeSourceFromCache() 去获取原图

class DecodeJob<A, T, Z> {

    ///从磁盘decode转化过的resource,然后transcode
    public Resource<Z> decodeResultFromCache() throws Exception {
        if (!diskCacheStrategy.cacheResult()) {
            return null;
        }

        long startTime = LogTime.getLogTime();
        ///Resource<GifBitmapWrapper>
        Resource<T> transformed = loadFromCache(resultKey);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Decoded transformed from cache", startTime);
        }
        startTime = LogTime.getLogTime();
        ///Resource<GlideDrawable>
        Resource<Z> result = transcode(transformed);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Transcoded transformed from cache", startTime);
        }
        return result;
    }

    public Resource<Z> decodeSourceFromCache() throws Exception {
        if (!diskCacheStrategy.cacheSource()) {
            return null;
        }

        long startTime = LogTime.getLogTime();
        ///Resource<GifBitmapWrapper> 使用的是OriginalKey
        Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Decoded source from cache", startTime);
        }
        return transformEncodeAndTranscode(decoded);
    }

    private Resource<T> loadFromCache(Key key) throws IOException {
        File cacheFile = diskCacheProvider.getDiskCache().get(key);
        if (cacheFile == null) {
            return null;
        }

        Resource<T> result = null;
        try {
            ///FileToStreamDecoder,将file解码成Resource<GifBitmapWrapper>实例是GifBitmapWrapperResource
            result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
        } finally {
            if (result == null) {
                diskCacheProvider.getDiskCache().delete(key);
            }
        }
        return result;
    }
}

decodeResultFromCache() 方法和 decodeSourceFromCache() 方法内部都调用了 loadFromCache() 方法来获取磁盘缓存,只不过前者使用的参数是 resultKey 而后者使用的是 OriginalKey。
它们得到的都是一个缓存文件,只不过前者写入的内容是变换后的图片,后者后者写入的内容是原始的图片。所以前者获取文件之后只需要进行解码和转码即可,而后者需要进行变换加上解码和转码。

decodeSourceFromCache() 方法中从磁盘缓存中获取原图之后调用了 transformEncodeAndTranscode() 方法进行变换和转码,这里也涉及到缓存的操作,这个会在后面讲到。

是否决定从磁盘缓存中获取取决于 DiskCacheStrategy,它是一个枚举类,用来表示一组缓存策略,cacheSource 属性表示是否缓存原图,cacheResult 属性表示是否缓存处理图。Glide 的默认的磁盘缓存是 RESULT,即只缓存处理图,也可以通过 diskCacheStrategy() 方法来指定。

public enum DiskCacheStrategy {
    /** Caches with both {@link #SOURCE} and {@link #RESULT}. */
    ALL(true, true),
    /** Saves no data to cache. */
    NONE(false, false),
    /** Saves just the original data to cache. */
    SOURCE(true, false),
    /** Saves the media item after all transformations to cache. */
    RESULT(false, true);

    private final boolean cacheSource;
    private final boolean cacheResult;

    DiskCacheStrategy(boolean cacheSource, boolean cacheResult) {
        this.cacheSource = cacheSource;
        this.cacheResult = cacheResult;
    }
}

往磁盘中写入原图

当从磁盘缓存中既取不到原图也取不到处理图时,才会发起网络请求去获取。相应的逻辑在 DecodeJob 的 decodeFromSource() 方法中。

在 decodeFromSource() 方法中,程序首先调用了 decodeSource() 方法来从网络中获取 InputStream 以及解码成 Bitmap,然后调用 transformEncodeAndTranscode() 来进行图片的变换和转码。这两个步骤中都涉及到对缓存的操作。先来看看 decodeSource() 方法

private Resource<T> decodeSource() throws Exception {
        Resource<T> decoded = null;
        try {
            long startTime = LogTime.getLogTime();
            ///ImageVideoFetcher,内部使用... 返回一个ImageVideoWrapper
            final A data = fetcher.loadData(priority);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Fetched data", startTime);
            }
            if (isCancelled) {
                return null;
            }
            ///返回 Resource<GifBitmapWrapper>
            decoded = decodeFromSourceData(data);
        } finally {
            fetcher.cleanup();
        }
        return decoded;
    }

    private Resource<T> decodeFromSourceData(A data) throws IOException {
        final Resource<T> decoded;
        ///判断磁盘缓存策略中是否缓存source,缓存ImageVideoWrapper并decode(原始InputStream)
        if (diskCacheStrategy.cacheSource()) {
            decoded = cacheAndDecodeSourceData(data);
        } else {
            ......
        }
        return decoded;
    }

    private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
        long startTime = LogTime.getLogTime();
        ///loadProvider是FixedLoadProvider,里面封装了ImageVideoGifDrawableLoadProvider
        // 这里实际返回的ImageVideoWrapperEncoder
        SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
        ///diskCacheProvider是一个LazyDiskCacheProvider,返回DiskLruCacheWrapper实例
        ///resultKey由Engine构造decodejob的时候传进来,由EngineKeyFactory.builidKey返回.
        diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Wrote source to cache", startTime);
        }

        startTime = LogTime.getLogTime();
        Resource<T> result = loadFromCache(resultKey.getOriginalKey());
        if (Log.isLoggable(TAG, Log.VERBOSE) && result != null) {
            logWithTimeAndKey("Decoded source from cache", startTime);
        }
        return result;
    }    

decodeSource() 方法中 DateFetcher 的 loadData() 方法从网络中获取数据之后,调用了 decodeFromSourceData() 方法开始进行解码。在 decodeFromSourceData() 方法中,解码前首先判断当前磁盘缓存策略,如果 cacheSource 为 true,那么解码前还有写入磁盘缓存的操作,也就是 cacheAndDecodeSourceData() 方法。方法中获取了 DiskLruCacheWrapper 对象然后调用了它的 put() 方法来写入缓存,最终会将从服务器获取的 InputSteream 写入一个缓存文件中,缓存对应的 Key 是由 Url 生成的 OriginalKey。

往磁盘缓存中写入处理图

回到 decodeSource() 方法,将图片解码之后,接着就是调用 transformEncodeAndTranscode() 进行变换和转码,这里也涉及到缓存的操作

private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
        long startTime = LogTime.getLogTime();
        ///因为decodeSourceFromCache去取出的resource还没有经过transform,要先transform再缓存起来
        Resource<T> transformed = transform(decoded);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Transformed resource from source", startTime);
        }

        ///将transform后的resource写入缓存 bitmap或GifDrawable
        writeTransformedToCache(transformed);

        startTime = LogTime.getLogTime();
        ///与decodeResultFromCache的第二步一样
        Resource<Z> result = transcode(transformed);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Transcoded transformed from source", startTime);
        }
        return result;
    }

    private void writeTransformedToCache(Resource<T> transformed) {
        if (transformed == null || !diskCacheStrategy.cacheResult()) {
            return;
        }
        long startTime = LogTime.getLogTime();
        ///GifBitmapWrapperResourceEncoder
        SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
        ///resultKey是一个EngineKey
        diskCacheProvider.getDiskCache().put(resultKey, writer);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Wrote transformed from source to cache", startTime);
        }
    }

transformEncodeAndTranscode() 方法中调用了 transform() 方法进行图片变换之后调用了 writeTransformedToCache() 方法来尝试将 Resource 写入磁盘缓存。

写入内存缓存

按着图片的加载流程,经过变换和转码之后,会回到 EngineRunnable 的 run() 方法,然后回调 EngineJob 的 onResourceReady() 方法,通知图片获取已经完成。我们直接来到 handleResultOnMainThread() 方法,这时 EngineJob 正准备将 onResourceReady() 回调分发给 GenericRequest。

private void handleResultOnMainThread() {
    if (isCancelled) {
        resource.recycle();
        return;
    } else if (cbs.isEmpty()) {
        throw new IllegalStateException("Received a resource without any callbacks to notify");
    }
    ///封装成一个EngineResource
    engineResource = engineResourceFactory.build(resource, isCacheable);
    hasResource = true;

    // Hold on to resource for duration of request so we don't recycle it in the middle of notifying if it
    // synchronously released by one of the callbacks.
    engineResource.acquire();
    ///key是一个EngineKey。如果resource.isCacheable()为true,resource将添加到activeResources中,并在移除时加入memorycache
    listener.onEngineJobComplete(key, engineResource);

    ///这里的ResourceCallback就是GenericRequest,由GenericRequest调用EngineJob的load方法时传入
    for (ResourceCallback cb : cbs) {
        if (!isInIgnoredCallbacks(cb)) {
            engineResource.acquire();
            ///回调GenericRequest
            cb.onResourceReady(engineResource);
        }
    }
    // Our request is complete, so we can release the resource.
    engineResource.release();
}

public void onEngineJobComplete(Key key, EngineResource<?> resource) {
    Util.assertMainThread();
    // A null resource indicates that the load failed, usually due to an exception.
    if (resource != null) {
        ///监听engineresource的释放,从activeResources中移除,移除时放入MemoryCache中
        resource.setResourceListener(key, this);

        if (resource.isCacheable()) {
            ///将resource放到activeResources中
            activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
        }
    }
    // TODO: should this check that the engine job is still current?
    jobs.remove(key);
}

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

注意 listener.onEngineJobComplete(key, engineResource) ,这里的 listener 正是 EngineJob 自身。在 EngineJob 的 onEngineJobComplete() 方法中给 EngineResource 设置了一个 ResourceListener,并且如果没有设置跳过内存缓存,将 EngineResource 放入 activeResources 中。那么 ResourceListener 做了什么呢?

ResourceListener 接口中只有 onResourceReleased() 一个方法,当 EngineResource 需要被释放时会回调该方法来通知监听者。这里的监听者正是 EngineJob,在 EngineJob 的 onResourceReleased() 方法中,将 EngineResource 从 activeResources 中删除,并且如果没有设置跳过内存缓存,则放入 MemoryCache 中,否则回收 EngineResource。这讲导致 EngineResource 中持有的 Bitmap 回收到 BitmapPool 中,等待复用。

接着图片将显示到 Target 上,加载流程结束,其中涉及的缓存操作也分析完毕。

总结

内存缓存

内存缓存包括 LruResourceCache 和 ActiveResources 存储的对象都是 EngineResource 对象。它是 Resource 对象的封装,并且内部维护对 Resource 的引用计数。调用 acquire() 引用计数+1,调用 release() 引用计数-1.当调用 release() 后引用计数为0时,由 ResourceListener(目前只有 Engine)来将其从 ActiveResources 移除并决定放入 LruResourceCache 还是调用 Resouece 的 recycler() 方法来回收相关资源,例如将 Bitmap 放入 BitmapPool。

LruResourceCache

LruResourceCache 用于保存最近被使用但是当前不在使用的资源。它的最大容量与屏幕分辨率和 Bitmap 质量参数有关,默认是 宽度 Pixels高度 PiexlsARGB_8888图片的质量参数*2,这样至少足够缓存两个屏幕大小的图片了。当容量过大时,使用 LRU 算法来移除最近最少使用的缓存项。( LruResourceCache 和 BitmapPool 默认容量大小是由一个MemorySizeCalculator的类来计算)

LruResourceCache 用于保存最近被使用但是当前不在使用的资源。其中缓存的来源只有一个:ActiveResources。当 ActiveResources 中的缓存不再被使用时,会被移除,放入 LruResourceCache 中。缓存的去向有两个:一个是缓存通过 remove() 被读取时,转移到 ActiveResources 中,第二个是最近使用较少被 LRU 算法移除,然后相关的 Bitmap 资源回收到 BitmapPool。

ActiveResources

虽说 ActiveResources 中的资源是当前正在使用的资源,Glide 也提供了负责清理 ActiveResources 弱可达资源的实现(弱引用中保存的对象可能在系统资源紧张时被清除了,但是弱引用本身还存在,所以需要在恰当的时机清除)。实现方法是构造 EngineResource 的弱引用 WeakReference 时传进 ReferenceQueue 来监听弱引用的回收。当系统检测到该 EngineResource 是弱可达,即不在被使用时,就将该弱引用放入 ReferenceQueue 中。如果 ReferenceQueue 不为空,就说明 EngineResource 该被回收了,可是在什么时候回收呢?

Glide 在当前线程对应 Looper 所对应的 MessageQueue 中通过 addIdleHandler() 方法添加了一个 IdleHandler 实例 RefQueueIdleHandler,当 MessageQueue 空闲的时候就会回调 IdleHandler 的 queueIdle() 方法,在 queueIdle() 方法中清理保存的引用对象为空的 WeakReference对象

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 static class ResourceWeakReference extends WeakReference<EngineResource<?>> {
    private final Key key;

    public ResourceWeakReference(Key key, EngineResource<?> r, ReferenceQueue<? super EngineResource<?>> q) {
        super(r, q);
        this.key = key;
    }
}

private ReferenceQueue<EngineResource<?>> getReferenceQueue() {
    if (resourceReferenceQueue == null) {
        resourceReferenceQueue = new ReferenceQueue<EngineResource<?>>();
        MessageQueue queue = Looper.myQueue();
        queue.addIdleHandler(new RefQueueIdleHandler(activeResources, resourceReferenceQueue));
    }
    return resourceReferenceQueue;
}

// Responsible for cleaning up the active resource map by remove weak references that have been cleared.
private static class RefQueueIdleHandler implements MessageQueue.IdleHandler {
    private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
    private final ReferenceQueue<EngineResource<?>> queue;

    public RefQueueIdleHandler(Map<Key, WeakReference<EngineResource<?>>> activeResources,
            ReferenceQueue<EngineResource<?>> queue) {
        this.activeResources = activeResources;
        this.queue = queue;
    }

    @Override
    public boolean queueIdle() {
        ResourceWeakReference ref = (ResourceWeakReference) queue.poll();
        if (ref != null) {
            activeResources.remove(ref.key);
        }

        return true;
    }
}

BitmapPool

BitmapPool 的最大容量与屏幕分辨率有关,默认是 宽度 Pixels高度 PiexlsARGB_8888图片的质量参数*4,这样至少足够缓存四个屏幕大小的图片。容量到达阈值时,使用 LRU 算法从最近最少使用的图片尺寸中移除图片。

复用策略

BitmapPool 使用策略模式来封装不同的复用策略,策略接口是 LruPoolStrategy,定义了 put()、get()、getSize() 等方法。Glide 中有两种复用策略

AttributeStrategy 复用的图片需要图片的尺寸和 Bitmap.Config 完全一致
SizeConfigStrategy 复用要求相对宽松,复用的图片需要 Bitmap.Config 一致,但复用图片的所需内存比原图小即可。确保 Bitmap.Config 一致后,如果有内存大小一致的图片则直接复用,没有则选取内存稍大一点的图片。需要 KITKAT 以上版本
Glide 在 KITKAT 以上系统中采用 SizeConfigStrategy,否则采用 AttributeStrategy。显然采取 SizeConfigStrategy 的复用率更高。之所以有这两者的区分,跟 Bitmap 的 reconfigure() 方法有关,BitmapPool 能够复用旧图片的内存得益于这个方法

简单来说,reconfigure() 方法能够在不重新初始化 Bitmap 和不影响底层内存分配的前提下修改 Bitmap 的尺寸和类型。使用该方法能够复用旧 Bitmap 的内存从而避免给新 Bitmap 分配内存。通过复用生成的 Bitmap,新的内存大小可以通过 getByteCount() 方法获得。

由于 Bitmap 是复用的,所以它所映射的底层内存中还是原始图片的数据,所以 BitmapPool 将 Bitmap 返回给使用者前,还需要使用 Bitmap 的 eraseColor(Color.TRANSPARENT) 来擦除旧的数据。

磁盘缓存

磁盘缓存的默认路径在 /data/data/\/cache/image_manager_disk_cache,默认的大小为 250MB,默认的实现类是 DiskLruCacheWrapper。

Glide 的磁盘缓存默认只缓存处理图,不过可以根据需要通过设置 DiskCacheStrategy 来定制缓存策略。Glide 中不管是原图还是处理图,每张图片都与一个缓存文件对应,存取原图时使用 OriginalKey,内部封装了图片的 Url 和 Signature 信息。而存取处理图时使用 EngineKey 内部封装了图片的 Url 和 Signature 之外,还包括图片尺寸和图片处理流程中相关的 Decoder、Encoder、Transformation 所返回的 id,,通过这些信息就能唯一确定一张图片了。

DiskLruCacheWrapper

DiskLruCacheWrapper 实现了 DiskCache 接口,提供的方法非常简洁,提供了 get()、put()、delete()、clear() 四个方法。DiskLruCacheWrapper 内部封装了 DiskLruCache 对象,缓存文件的管理由它来完成。

该包装类主要的作用的是将 Key 中封装的信息(原图就是图片的 Url,处理图就是 Url、尺寸、Decoder等)通过 SHA-256 算法进行编码生成一个safekey 然后再用这个key去调用实际的DiskLruCache的get/remove/clear 和DiskLruCache.editor写入缓存文件。

DiskLruCache

DiskLruCache内部也是一个LinkedHashMap,采用Lru缓存策略来维护缓存
Entry 用来表示一个缓存实体,封装了一些相关信息。我们看看它的构造函数

private Entry(String key) {
    this.key = key;
    this.lengths = new long[valueCount];
    cleanFiles = new File[valueCount];
    dirtyFiles = new File[valueCount];

    // The names are repetitive so re-use the same builder to avoid allocations.
    StringBuilder fileBuilder = new StringBuilder(key).append('.');
    int truncateTo = fileBuilder.length();
    for (int i = 0; i < valueCount; i++) {
        fileBuilder.append(i);
        cleanFiles[i] = new File(directory, fileBuilder.toString());
        fileBuilder.append(".tmp");
        dirtyFiles[i] = new File(directory, fileBuilder.toString());
        fileBuilder.setLength(truncateTo);
    }
    }

可见 Entry 封装了 Key,对应的缓存文件和文件大小等信息。一个缓存实体对应两个缓存文件,cleanFile 文件中的数据时刻是干净可读的,dirtyFile 的作用是作为临时文件,当需要写入缓存时,返回的是 dirtyFile,等调用 commit() 方法时再更新数据。这样缓存的写入时也不会影响缓存的读取了。dirtyFile 用 key.1命名,dirtyFiles 用 key.1.tmp 命名,知道缓存 Key 就知道对应的缓存文件了。

磁盘缓存与内存缓存不同,内存缓存在程序每次重新启动时都是空白的全新的状态,需要一个预热的过程,等缓存写入一定量的时候才能开始发挥作用,而磁盘缓存则不能受程序重新启动的影响,程序重新启动时,要能够自动恢复到上次运行的状态,每个 Key 对应哪个缓存文件、每个缓存的大小、哪些缓存文件的数据是干净的、哪些缓存文件的数据是不正常的写入需要删除、当前磁盘缓存的大小等等,都需要实时管理好。这就需要有一个文件实时记录下磁盘缓存的相关信息,以便能够随时恢复。DiskLruCache 就用到了这样一个文件,文件名为 journal。该文件记录当前DiskLruCache所管理的缓存文件的操作记录,以便于在程序初始化时构建DiskLruCache对象,包括所有缓存文件的key值(通过这个可以找到对应的缓存文件)缓存文件的数据状态(脏数据还是干净数据)
DiskLruCache已经是一个标准类了(已获Google官方认证)这里就不再分析了,感兴趣的话可以直接看源码或者看郭霖大神的这篇文章
Android DiskLruCache完全解析,硬盘缓存的最佳方案

最后

谢谢阅读!