关于Glide磁盘缓存问题

一开始是想记录下如何设置磁盘的缓存大小和位置,结果看到一篇文章介绍的很好,就自己跟着源码看了一遍也记录一下吧。

一、Glide加载资源流程

  • Glide使用了ActiveResources(活动缓存弱引用)+MemoryCache(内存缓存Lru算法)+DiskCache(磁盘缓存Lru算法)。

  • ActiveResources:存储当前界面使用到的图片。界面不展示后,该Bitmap又被缓存至MemoryCache中,并从ActiveResources中删除。

  • Memory Cache:存储当前没有使用到的Bitmap,当MemoryCache中得到Bitmap后,该Bitmap又被缓存至ActiveResources中,并从MemoryCache中删除。

  • Disk Cache:持久缓存。例如图片加圆角,处理后图片会被缓存到文件中,应用被再次打开时可以加载缓存直接使用。

注意: ActiveResources + MemoryCache是内存缓存,都属于运行时缓存,且互斥(同一张图片不会同时缓存在ActiveResources+MemoryCache),应用被杀死后将不存在。

二、Glide 的缓存分层结构

三、Glide缓存相关类的关联关系

  • Engine: 是个单例,负责具体执行内存缓存获取逻辑,并触发磁盘缓存的获取流程 (磁盘缓存的获取流程其实是由内部的 EngineJob 和 EngineRunnable 代理)。
  • ActiveResources: 第一层缓存(内存缓存),主要缓存的是屏幕正在使用中的图片,它使用的是弱引用进行缓存,它与Engine是一对一的关系。这里就有一个面试题,为什么要使用弱引用缓存,强引用可以吗,为什么?
  • LruResourceCache: 第二层缓存(内存缓存),主要是缓存ActiveResources中引用计数为0的数据,它与Engine是一对一的关系。
  • EngineJob: 每个图片请求都对应一个 EngineJob,它主要负责从磁盘获取缓存后将数据回传给Request的逻辑。它对Engine是多对一的关系。
  • EngineRunnable: 是一个获取缓存的任务,它与 EngineJob 是一对一的关系。
  • DecodeJob: 主要功能是负责从第三层到第5层缓存的获取并进行解码,在 EngineRunnable 中会执行 DecodeJob 对应的方法进行缓存的获取。内部含5部分能力。
    • 有提供编解码能力的 DataLoadProvider。
    • 提供数据源获取能力的 DataFetcher (第五层缓存)。
    • 提供数据处理的 Transformation。
    • 提供数据类型变换的 ResourceTranscoder。
    • 提供获取缓存文件能力的 DiskCacheProvider。
  • DiskCache:负责管理缓存文件的存/取 (第三、四层缓存)。

Lru内存缓存:使用LinkedHashMap来缓存资源(强引用),并设定一个缓存的大小。如果有资源被访问到,首先会在链表中删除该节点,然后再添加到链表头,这样就可以保证链表头部的节点是最近访问过的。而当缓存的数量达最大值的时候,就会将链表尾部(最近最少使用)的数据移除。 

四、Glide的缓存Key

介绍缓存流程之前先介绍下EngineKey

4.1 作用

缓存key是实现内存和磁盘缓存的唯一标识

4.2 原理

重写equals和hashCode方法,来确保只有key对象的唯一性

4.3 生成Key

生成缓存Key的代码在Engine类的load()方法当中:

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

每一次的load方法内部,都会有一个fetcher.getId()方法获得了一个id字符串,这个字符串也就是我们要加载的图片的唯一标识,比如说如果是一张网络上的图片的话,那么这个id就是这张图片的url地址。然后,这个id会结合signature、width、height等等10个参数一起传入到EngineKeyFactory的buildKey()方法当中,从而构建出了一个EngineKey对象,这个EngineKey也就是Glide中的缓存Key了。 因此,如果你图片的width或者height发生改变,也会生成一个完全不同的缓存Key。

五、缓存的获取流程

上面我们介绍了缓存的层级、与缓存相关的几个类之间的关联关系,以及缓存Key,下面我们从源码的角度来分析一下这一过程。

主要从下面几个角度分析:

  • 缓存获取的入口。
  • 内存缓存的获取。
  • 磁盘缓存的获取。
5.1 缓存获取的入口

Glide 获取图片的操作类似网络请求,下面我们就先来分析下缓存获取的入口

public <Y extends Target<TranscodeType>> Y into(Y target) {
        ...
        Request request = buildRequest(target);
        target.setRequest(request);
        lifecycle.addListener(target);
        requestTracker.runRequest(request);

        return target;
    }

由.into(iv)开始会创建一个Request,继续跟进源码发现创建的是GenericRequest

// GenericRequest.class
public void onSizeReady(int width, int height) {
    status = Status.RUNNING;
    width = Math.round(sizeMultiplier * width);
    height = Math.round(sizeMultiplier * height);
	// 获取ModelLoader
    ModelLoader<A, T> modelLoader = loadProvider.getModelLoader();
    // 获取 DataFetcher
    final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height);
    // 获取 ResourceTranscoder
    ResourceTranscoder<Z, R> transcoder = loadProvider.getTranscoder();

    loadedFromMemoryCache = true;
    // 调用 Engine.load() 方法执行缓存加载逻辑。
    loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
            priority, isMemoryCacheable, diskCacheStrategy, this);
}
  • 在调用 Engine.load() 方法之前,就从注册表里找到了与加载数据相关的 ModelLoader、DataFetcher、ResourceTranscoder。
  • 此处的 loadProvider 是 FixedLoadProvider 类型,为什么不直接传入 load 方法内?
  • load() 方法接收 DataLoadProvider 类型参数,遵循类的单一职责。
  • load() 方法传入的最后一个参数类型为 ResourceCallback,它是 Request 与 Engine 关联的纽带。
5.2 内存缓存的查询

前面我们已经提到了获取缓存的入口,下面我们分析一下 Engine.load() 方法具体做了些什么。

// Engine.class
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) {
	// 1.生成EngineKey,它用于第1,2,3层缓存的查找。里面包含了所需图片的尺寸、解码器等信息。
    final String id = fetcher.getId();
    EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
            loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
            transcoder, loadProvider.getSourceEncoder());
	// 2.先从第2层缓存中查找。
    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
        cb.onResourceReady(cached);
        return null;
    }
	// 2.再从第1层缓存中查找。
    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
        cb.onResourceReady(active);
        return null;
    }
	
	// jobs 是一个Map集合,维护了当前正在执行数据请求的任务。当需要取消所有任务时,方便操作。
	// 3.如果同一个图片被请求两次,会将第二次的请求直接关联到第1次的请求上,避免重复请求。
    EngineJob current = jobs.get(key);
    if (current != null) {
        current.addCallback(cb);
        return new LoadStatus(cb, current);
    }
	// 4.执行磁盘缓存查询逻辑。
    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);
    // 这里启动EngineRunnable任务。
    engineJob.start(runnable);

    return new LoadStatus(cb, engineJob);
}

// 第二层内存缓存查找逻辑
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
    //...
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
        cached.acquire();
        // 缓存到第一层activeResources内存缓存中。
        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) {
    //...
    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;
}
  • Engine.load() 方法主要处理了2层内存缓存的查询逻辑。先从第二层内存缓存中查询,再从第一层内存缓存中查询。
  • 如果从第二层内存缓存中找到缓存后,要将缓存从第二层缓存移除,并加入到第一层缓存中。
  • 第一层缓存使用弱应用进行缓存,便于当弱引用中的数据没有被使用,且仍缓存在第一层缓存中时被回收。如果使用强引用代替,则当该缓存资源没有被使用时,占用的内存资源也不能被释放。
  • Engine 中通过一个 Map 集合缓存当前正在执行的 EngineJob。当需要取消所有任务时,方便操作。
  • 缓存 EngineJob 的另一个好处是:如果同一个图片被请求两次,会将第二次的请求直接关联到第1次的EngineJob 上,避免重复请求。
  • 执行后面三层磁盘缓存查询逻辑。
5.3 磁盘缓存的查询

上面介绍了磁盘缓存的查询流程具体交给 EngineJob 和 EngineRunnable来执行,下面我们就来分析一下

// Engine.load 方法
// EngineJob 主要是负责图片缓存获取到之后的逻辑处理,如回传给Request。
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是具体执行加载逻辑的类,所以要将EngineJob传入,方便将获取到的缓存数据回传给EngineJob,继而传递到Request中。
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
// 里面是一个线程池,用于执行EngineRunnable任务。
engineJob.start(runnable);
  • EngineJob 内部持有两个线程池。
    • DiskCacheService 线程池:用于 ResultCache、SourceCache 的缓存加载解析。
    • SourceSrevice 线程池:用于 Source 数据源的加载解析。
  • EngineRunnable 内传入 EngineJob:EngineJob 主要负责缓存获取后的逻辑处理,并与 Request 通信。EngineRunnable 是具体执行缓存加载逻辑的类,所以要将 EngineJob 传入,是方便获取到缓存时通过 EngineJob 传递到 Request 中。

下面我们重点看下 EngineRunnable.run() 方法。

// EngineRunnable.class
public void run() {
    //...
    Resource<?> resource = null;
    try {
    	// 1.获取缓存数据并使用Decoder进行解析。
        resource = decode();
    } catch (Exception e) {
    }
    //...
    if (resource == null) {
    	// 数据获取失败
        onLoadFailed(exception);
    } else {
    	// 数据获取成功
        onLoadComplete(resource);
    }
}

// stage 默认是Stage.CACHE。
this.stage = Stage.CACHE;
private boolean isDecodingFromCache() {
    return stage == Stage.CACHE;
}

private Resource<?> decode() throws Exception {
	// 是否是从磁盘缓存中解析,默认是true。
    if (isDecodingFromCache()) {
        return decodeFromCache();
    } else {
        return decodeFromSource();
    }
}
    
private void onLoadFailed(Exception e) {
    if (isDecodingFromCache()) {
    	// 如果当stage为Stage.CACHE时,走到onLoadFailed逻辑,说明磁盘缓存里没有数据,因此将stage设置为Stage.SOURCE,并重新执行EngineRunnable.run()方法。
        stage = Stage.SOURCE;
        // SourceService 线程池
        manager.submitForSource(this);
    } else {
        manager.onException(e);
    }
}

private void onLoadComplete(Resource resource) {
    manager.onResourceReady(resource);
}

// ----------------  Another Class --------------------
// EnginJob.class
public void onResourceReady(final Resource<?> resource) {
    this.resource = resource;
    MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
}

private void handleResultOnMainThread() {
	//...
    engineResource = engineResourceFactory.build(resource, isCacheable);
    hasResource = true;
    engineResource.acquire();
    // 1.保存到第一层内存缓存
    listener.onEngineJobComplete(key, engineResource);
    for (ResourceCallback cb : cbs) {
        if (!isInIgnoredCallbacks(cb)) {
            engineResource.acquire();
            // 2.回传给Request的Callback
            cb.onResourceReady(engineResource);
        }
    }
    engineResource.release();
}
  • EngineRunnable.run() 方法的流程比较简单,主要分为2个步骤:
    • 查询缓存并进行 decode 解析。
    • 判断返回值是否存在,不存在就执行 onLoadFailed 逻辑,成功就执行 onLoadComplete 逻辑。
  • isDecodingFromCache() 方法用于判断当前解析的数据是来自缓存还是来自第五次缓存(数据源)。
  • onLoadFailed() 方法:当stage为Stage.CACHE时,触发了onLoadFailed逻辑,说明磁盘缓存里没有数据,因此将stage设置为Stage.SOURCE,并重新执行EngineRunnable.run()方法,因此会从第五次缓存(数据源)查找数据。
  • onLoadComplete() 方法:decode() 方法获取到数据后,会执行 onLoadComplete 方法。该方法内的 manager 为 EnginJob 对象,所以会调用 EnginJob.onResourceReady() 方法将数据回传到 Request。
  • EngineRunnable 里的 stage 有点像状态机,通过为stage设置不同状态并重新触发EngineRunnable任务来实现。

接下来我们重点看下 EngineRunnable.decodeFromCache()、EngineRunnable.decodeFromSource 两个方法。

// EngineRunnable.class
private Resource<?> decodeFromCache() throws Exception {
    Resource<?> result = null;
    try {	
    	//1.先从ResultCache获取缓存。
        result = decodeJob.decodeResultFromCache();
    } catch (Exception e) {
    }
    if (result == null) {
    	//2.再从SourceCache获取缓存。
        result = decodeJob.decodeSourceFromCache();
    }
    return result;
}
  • 磁盘缓存分为 ResultCache、SourceCache 两部分,所以这里的逻辑也分两步来执行。
    • ResultCache:经过 transform 处理之后的数据。
    • SourceCache:从 Source 加载的数据,未经过任何处理。
  • EngineRunnable.decodeFromCache() 方法会先从 ResultCache 中获取,如果没有缓存,再从SourceCache中获取缓存。
//DecodeJob.class
public Resource<Z> decodeResultFromCache() throws Exception {
    if (!diskCacheStrategy.cacheResult()) {
        return null;
    }
    // 1.从DiskLruCache中获取ResultCache缓存,这里的key为EngineKey。
    Resource<T> transformed = loadFromCache(resultKey);
    // 2.将Resource<T>类型的数据transformed转码变成另一个类型(Resource<Z>)的数据result。
    // transcode()方法内调用transcoder.transcode(transformed)。
    Resource<Z> result = transcode(transformed);
    return result;
}

private Resource<T> loadFromCache(Key key) throws IOException {
	// 1.1 从DiskLruCache中获取缓存。
    File cacheFile = diskCacheProvider.getDiskCache().get(key);
    if (cacheFile == null) {
       return null;
    }

    Resource<T> result = null;
    try {
    	// 1.2 通过解码器对缓存文件进行解码。
       result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
    } finally {
       if (result == null) {
           diskCacheProvider.getDiskCache().delete(key);
       }
    }
    return result;
}
  1. loadFromCache(resultKey) 方法:从DiskLruCache中获取 ResultCache 缓存,这里的 key 为 EngineKey。
    1. 通过 DiskLruCache 获取 ResultCache 缓存文件。
    2. 通过解码器对缓存文件进行解码。
  2. transcode() 方法:将Resource类型的数据转码变成另一个类型(Resource)的数据。
// DecodeJob.class
public Resource<Z> decodeSourceFromCache() throws Exception {
    if (!diskCacheStrategy.cacheSource()) {
        return null;
    }
    // 1.从DiskLruCache中获取SourceCache缓存,这里的key为OriginalKey,
    // 这部分前面已经分析过了。
    Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
    // 2.执行transform、Encoder、Transcode操作。
    return transformEncodeAndTranscode(decoded);
}

private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
	// 2.1 执行transform操作。我们外部传入的transform就是在这里执行的。
    Resource<T> transformed = transform(decoded);
    // 2.2 将内存数据写入的磁盘中,生成ResultCache缓存。
    writeTransformedToCache(transformed);
    // 2.3 将Resource<T>类型的数据转码变成另一个类型(Resource<Z>)的数据,这部分前面已经分析过了。
    Resource<Z> result = transcode(transformed);
    return result;
}

private void writeTransformedToCache(Resource<T> transformed) {
	//...
	// 将内存数据写入的磁盘中,生成ResultCache缓存。
    SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
    diskCacheProvider.getDiskCache().put(resultKey, writer);
}
1.从 DiskLruCache 中获取 SourceCache 缓存,这里的 key 为 OriginalKey。
    1.1 通过 DiskLruCache 获取 SourceCache 缓存文件。
    1.2 通过解码器对缓存文件进行解码。
2.执行transform、Encoder、Transcode操作。
    2.1 执行transform操作。我们外部传入的transform就是在这里执行的。
    2.2 将内存数据写入的磁盘中,生成ResultCache缓存。
    2.3 将Resource类型的数据transformed转码变成另一个类型(Resource)的数据result。
关于 DiskLruCache 的内容可以参考:DiskLruCache 源码分析
  

 下面我们分析最后一种常见,直接从数据源加载数据。 

// EngineRunnable.class
private Resource<?> decodeFromSource() throws Exception {
	// 从Source数据源获取缓存
    return decodeJob.decodeFromSource();
}

// ----------------  Another Class --------------------
// DecodeJob.class
public Resource<Z> decodeFromSource() throws Exception {
	// 1.加载并解析数据源(加载数据+对数据解码+SourceCache缓存)。
    Resource<T> decoded = decodeSource();
    // 2.执行transform、Encoder、Transcode操作。
    return transformEncodeAndTranscode(decoded);
}

private Resource<T> decodeSource() throws Exception {
    Resource<T> decoded = null;
    try {
    	// 1.1 使用DataFetcher加载数据源数据。
        final A data = fetcher.loadData(priority);
        if (isCancelled) {
            return null;
        }
        // 1.2 将数据到SourceCache缓存层,并重新触发第4层缓存的查找逻辑。
        decoded = decodeFromSourceData(data);
    } finally {
        fetcher.cleanup();
    }
    return decoded;
}


private Resource<T> decodeFromSourceData(A data) throws IOException {
    final Resource<T> decoded;
    if (diskCacheStrategy.cacheSource()) {
    	// 使用 DiskLruCache 进行数据缓存,并重新触发从第4层缓存查找逻辑。
        decoded = cacheAndDecodeSourceData(data);
    } else {
        decoded = loadProvider.getSourceDecoder().decode(data, width, height);
    }
    return decoded;
}

private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
	// 1.2.1 使用 DiskLruCache 进行数据缓存。
    SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
    diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
    // 1.2.2 执行第4层缓存的查找逻辑(前面已经介绍过)。
    Resource<T> result = loadFromCache(resultKey.getOriginalKey());
    return result;
}

1.加载并解析数据源(加载数据+对数据解码+SourceCache缓存)。
    1.1 使用DataFetcher加载数据源数据。
    1.2 将数据到SourceCache缓存层,并重新触发第4层缓存的查找逻辑。
        1.2.1 使用 DiskLruCache 进行数据缓存 (缓存到4级磁盘缓存)。
        1.2.2 执行第4层缓存的查找逻辑:根据OriginalKey从 DiskLruCache 中查找缓存文件,并进行解码。
2.执行transform、Encoder、Transcode操作。
    2.1 执行transform操作。我们外部传入的transform就是在这里执行的。
    2.2 将内存数据写入的磁盘中,生成ResultCache缓存 (缓存到3级磁盘缓存)。
    2.3 将Resource类型的数据transformed转码变成另一个类型(Resource)的数据result。

5.4 缓存查询流程图 

六、关于磁盘缓存如何配置磁盘缓存大小和位置 

需要实现GlideModule或AppGlideModule并在AndroidManifest.xml配置

public class MyAppGlideModule implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        // 配置磁盘缓存大小和位置
        int diskCacheSize = 1024 * 1024 * 500;// 500M
        String diskCacheDir = context.getCacheDir() + "/image_cache";
        // 创建磁盘缓存
        DiskLruCacheFactory diskCacheFactory = new DiskLruCacheFactory(diskCacheDir, diskCacheSize);
        builder.setDiskCache(diskCacheFactory);
    }

    @Override
    public void registerComponents(Context context, Glide glide) {
        //配置glide使用okhttp
        glide.register(GlideUrl.class, InputStream.class,new OkHttpUrlLoader.Factory());
    }
}
<application>
...
<meta-data
            android:name="xxx.xxx.MyAppGlideModule"
            android:value="GlideModule" />
...
</application>

七、总结

  1. 了解 Glide 的缓存分层结构,及与 Request 是如何关联的。
  2. 了解缓存的查询流程,特别是要注意优先查询第二层内存缓存。
  3. 了解缓存的保存逻辑。
    1. 第二层内存缓存 -> 第一次内存缓存
    2. 第五层数据源缓存 -> 第四层磁盘缓存 -> 第一次内存缓存
    3. 第四层磁盘缓存 -> 第三层磁盘缓存 -> 第一次内存缓存
  4. 了解缓存查询相关类的关联关系,及其作用。
  5. 了解缓存的数据变换操作 (ResourceTranscoder)。

原文链接:https://blog.csdn.net/Love667767/article/details/127601652

  • 14
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值