【Glide】缓存实现-内存和磁盘缓存

1,261 阅读4分钟

简介

本文主要是初步了解下 Glide 的缓存方式及实现,主要分为以下几个部分:

  1. Glide 的缓存方式介绍
  2. 内存与磁盘缓存原理
  3. 缓存算法的具体实现

Glide 的缓存方式介绍

Glide 缓存方式主要有两种,即内存缓存和磁盘缓存。内存缓存方面,根据数据是否当前正在使用,又可以细分为 Active 和 非 Active。磁盘缓存方面,根据是否进行过形变,又可以细分为 Resource Cache「指经过形变后的缓存」 和 Data Cache「指源数据缓存」。这里先简单了解内存和磁盘缓存。

//GlideBuilder.java
@NonNull
Glide build(@NonNull Context context) {
    //省略部分代码...
  	//内存缓存的实现
    if (memoryCache == null) {
      memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
    }
		//磁盘缓存的工厂实现
    if (diskCacheFactory == null) {
      diskCacheFactory = new InternalCacheDiskCacheFactory(context);
    }

    if (engine == null) {
      engine =
          new Engine(
              memoryCache,
              diskCacheFactory,
              diskCacheExecutor,
              sourceExecutor,
              GlideExecutor.newUnlimitedSourceExecutor(),
              animationExecutor,
              isActiveResourceRetentionAllowed);
    }
		//省略部分代码...
    return new Glide(
        context,
        engine,
        memoryCache,
        bitmapPool,
        arrayPool,
        requestManagerRetriever,
        connectivityMonitorFactory,
        logLevel,
        defaultRequestOptionsFactory,
        defaultTransitionOptions,
        defaultRequestListeners,
        experiments);
  }
/*
memeory cache 的实现是 LruResourceCache, 
在创建后会作为参数分别传入 Engine 和 Glide 对象。
disk cache factory 的实现是 InternalCacheDiskCacheFactory, 
在创建后只作为参数传入 Engine 对象。
通过工厂最终创建的默认磁盘缓存是 DiskLruCacheWrapper。
*/

内存与磁盘缓存原理

从 LruResourceCache 和 DiskLruCacheWrapper 的实现上来说,其缓存原理都采用了 LRU 算法,结合源码实现具体点来看,

首先来看下内存缓存原理,

public class LruResourceCache extends LruCache<Key, Resource<?>> implements MemoryCache {
  //MemoryCache 定义了内存缓存的行为,具体哪些行为呢?
  /*
  1.缓存操作:put(Key key, Resource<?> resource)
  2.删除操作:remove(Key key)
  3.清空操作:clearMemory()
  */
  //LruCache<K, V> 是 LRU 的实现类,内部通过 LinkedHashMap 实现缓存数据的维护,
  //该类已经实现了 put, get, remove, clear 等操作,作为子类,
  //要实现 getSize(Y item) 方法来指定单个缓存数据的大小。LruCache 这个类的方法实现是关键。
  //作为 LruResourceCache 本身,剩下需要做的事情就不多了。
}

再来看下磁盘缓存原理,

public class DiskLruCacheWrapper implements DiskCache {
  //DiskCache 定义了磁盘缓存的行为,同 MemeoryCache 类似,它也定义了一些操作方法
  /*
  1.缓存操作:put(Key key, Writer writer)
  2.删除操作:delete(Key key)
  3.清空操作:clear()
  4.获取操作:get(Key key),这步在 MemoryCache 里没有定义,但在 LruCache 里是定义并实现的。
  除了上面的行为操作,该接口还定义了两个接口,
  一个是用来创建磁盘缓存的工厂接口,一个是用来缓存读写操作的 Writer。
  */
  //同 LruCache 类似,这个是磁盘缓存的 LRU 实现类。
  private DiskLruCache diskLruCache;
  
  private synchronized DiskLruCache getDiskCache() throws IOException {
    if (diskLruCache == null) {
      //diskLruCache 对象通过 open 方法创建
      diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
    }
    return diskLruCache;
  }
}

DiskLruCache 会比内存缓存的 LruCache 复杂些,表现在:1.涉及到线程池操作,2.涉及到文件读写操作。但虽说有差别,核心点(用 LinkedHashMap 实现缓存集合)是一样的。

缓存算法的具体实现

在分析 Glide 缓存算法实现之前,先简单了解下 LRU 算法。

LRU(Least Recently Used)该算法是一种页面置换算法,对每个页面增加一个用来记录使用时间的字段,在存储空间满的情况下,根据比较使用时间字段,删除最近最少使用的页面,从而用来存放新页面。

从原理上可知:1.存储空间是受限的,也就是说有个存储空间初始值。2.需要一个数据结构能满足内容替换,也就是找出最近最少使用的元素。

LruCache 的 put 方法实现分析,

public class LruCache<T, Y> {
  private final Map<T, Entry<Y>> cache = new LinkedHashMap<>(100, 0.75f, true);
  
  public LruCache(long size) {
    //构造方法里做的就是初始化存储空间元素数量的多少(也就是总共能放多少页,放满了就要置换了)
    //注意!!!这个 size 是存储空间总大小,单位是和 getSize 是一致的
    /*
    Glide 用 MemorySizeCalculator 的 memoryCacheSize 字段对其进行的赋值,这个值的计算
    会根据设备屏幕尺寸有所不同。单位是 byte
    */
    this.initialMaxSize = size;
    this.maxSize = size;
  }
  
  @Nullable
  public synchronized Y put(@NonNull T key, @Nullable Y item) {
    //通过 getSize 方法获取到 item 元素的空间大小,以 byte 为单位
    final int itemSize = getSize(item);
    if (itemSize >= maxSize) {
      //这个的实现交给了子类
      onItemEvicted(key, item);
      return null;
    }

    if (item != null) {
      currentSize += itemSize;
    }
    //利用 LinkedHashMap 类型的 cache 存入 item 值
    //根据返回值判断 cache 中是否有旧的元素需要去除
    @Nullable Entry<Y> old = cache.put(key, 
    item == null ? null : new Entry<>(item, itemSize));
    if (old != null) {
      currentSize -= old.size;

      if (!old.value.equals(item)) {
        onItemEvicted(key, old.value);
      }
    }
    //检验空间是否足够,不够的话进行元素空间置换
    evict();

    return old != null ? old.value : null;
  }
  
  private void evict() {
    trimToSize(maxSize);
  }
  
  protected synchronized void trimToSize(long size) {
    Map.Entry<T, Entry<Y>> last;
    Iterator<Map.Entry<T, Entry<Y>>> cacheIterator;
    while (currentSize > size) {
      cacheIterator = cache.entrySet().iterator();
      //这里直接拿到的 cache 里的第一个元素,
      //因为是 LinkedHashMap,这样一来最近最少使用的就是第一个
      //循环释放,直到空间足够为止
      last = cacheIterator.next();
      final Entry<Y> toRemove = last.getValue();
      currentSize -= toRemove.size;
      final T key = last.getKey();
      cacheIterator.remove();
      onItemEvicted(key, toRemove.value);
    }
  }
  
  //用于包装存储元素的静态类,包含具体的元素及其空间大小
  static final class Entry<Y> {
    final Y value;
    final int size;

    @Synthetic
    Entry(Y value, int size) {
      this.value = value;
      this.size = size;
    }
  }
}

DiskLruCache 的 put 方法实现分析,

public final class DiskLruCache implements Closeable {
  //在通过 InternalCacheDiskCacheFactory 创建时指定了默认大小 250MB
  private long maxSize;
  private final LinkedHashMap<String, Entry> lruEntries =
      new LinkedHashMap<String, Entry>(0, 0.75f, true);
  public final class Editor {
    private final Entry entry;
    private final boolean[] written;
    private boolean committed;
    
    public File getFile(int index) throws IOException {
      synchronized (DiskLruCache.this) {
        if (entry.currentEditor != this) {
            throw new IllegalStateException();
        }
        if (!entry.readable) {
            written[index] = true;
        }
        File dirtyFile = entry.getDirtyFile(index);
        directory.mkdirs();
        return dirtyFile;
      }
    }
  }
}
//在磁盘缓存处理上,行为操作是通过 DiskLruCacheWrapper 对象操作的,来看下 DiskLruCacheWrapper 类
//DiskLruCacheWrapper
public class DiskLruCacheWrapper implements DiskCache {
  private DiskLruCache diskLruCache;
  
  @Override
  public void put(Key key, Writer writer) {
    //SafeKeyGenerator 确保能获取到一个 safeKey
    String safeKey = safeKeyGenerator.getSafeKey(key);
    try {
      try {
        DiskLruCache diskCache = getDiskCache();
        //通过 diskCache 获取 safeKey 对应的值
        Value current = diskCache.get(safeKey);
        if (current != null) {
          return;
        }
        //获取 safeKey 对应的编辑器
        DiskLruCache.Editor editor = diskCache.edit(safeKey);
        try {
          File file = editor.getFile(0);
          //通过 Writer 将文件数据写入到 file 中
          if (writer.write(file)) {
            //如果写入成功,就执行 commit 
            editor.commit();
          }
        }
      }
  }
}

DiskLruCache 比 LruCache 的实现会复杂一些,暂且先做简单了解,后续再进行深入分析。