Glide源码分析(二)——磁盘缓存实现

Glide中默认磁盘缓存实现的入口为DiskLruCacheFactory,DiskLruCacheFactory提供了几个接收不同参数的构造函数,以允许指定缓存文件的位置和磁盘缓存的大小。其中比较有意思的是,在构造函数中DiskLruCacheFactory并没有立即构建缓存的File,而是在build函数执行的时候才开始构建缓存File,这算是懒加载的一种吧,即在真正需要使用到缓存的时候才去申请相关的资源。
DiskLruCacheFactory的build方法返回一个DiskLruCacheWrapper,通过这个类的相关接口即可对磁盘缓存进行操作。

DiskLruCacheWrapper主要组成部分

DiskLruCacheWrapper的名字其实就反映了它的功能和实现,它只是一个装饰类,其缓存功能的实现主要依靠其中的DiskLruCache,这个类稍后我们会单独分析。DiskLruCacheWrapper在DiskLruCache的基础上通过SafeKeyGenerator保证了缓存key的唯一性;因为在DiskLruCache中只保证了操作其内部的journal日志和缓存数据结构lruEntries时的线程安全,并没有机制保证Editor对缓存文件操作的安全性,因此DiskLruCacheWrapper内部又实现了DiskCacheWriteLocker,以此来保证在同一时刻,一个缓存文件只会被一个线程修改,

SafeKeyGenerator的实现

DiskLruCacheWrapper主要使用SafeKeyGenerator来得到一个类似于url的唯一特征字符串来作为缓存key。虽然它的使命简单,但是实现得却并不简单。其内部主要有两个成员变量,loadIdToSafeHash是一个通过LinkedHashMap来实现的LRU缓存,主要用它来缓存最近生成的缓存key,避免频繁地对同一个url进行key的生成;digestPool的实现看起来比较绕,我们需要简单分析一下,首先看它的声明实现:

    private final Pools.Pool<PoolableDigestContainer> digestPool = FactoryPools.threadSafe(10,
      new FactoryPools.Factory<PoolableDigestContainer>() {
        @Override
        public PoolableDigestContainer create() {
          try {
            return new PoolableDigestContainer(MessageDigest.getInstance("SHA-256"));
          } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
          }
        }
      });

从它的声明实现来看,digestPool是通过FactoryPools构造出来的,传入的其中一个参数是一个可以创建PoolableDigestContainer的接口实现。跟进FactoryPools看相关的实现方法:

public static <T extends Poolable> Pool<T> threadSafe(int size, Factory<T> factory) {
  return build(new SynchronizedPool<T>(size), factory);
}


private static <T extends Poolable> Pool<T> build(Pool<T> pool, Factory<T> factory) {
  return build(pool, factory, FactoryPools.<T>emptyResetter());
}

private static <T> Pool<T> build(Pool<T> pool, Factory<T> factory,
Resetter<T> resetter) {
  return new FactoryPool<>(pool, factory, resetter);
}

原来是通过参数构造了一个FactoryPool的实例,再看看FactoryPool的实现:

FactoryPool(Pool<T> pool, Factory<T> factory, Resetter<T> resetter) {
  this.pool = pool;
  this.factory = factory;
  this.resetter = resetter;
}

@Override
public T acquire() {
  T result = pool.acquire();        // 由于pool的空间有限,可能会返回空
  if (result == null) {
    result = factory.create();          // pool中没有的时候从factory中构造,因此保证不会返回空
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      Log.v(TAG, "Created new " + result.getClass());
    }
  }
  if (result instanceof Poolable) {     // 如果构造的资源是支持回收使用的(poolable),由于这里刚构造出来,因此是有效的,没有被回收状态的
    ((Poolable) result).getVerifier().setRecycled(false /*isRecycled*/);
  }
  return result;
}

@Override
public boolean release(T instance) {
  if (instance instanceof Poolable) {
    ((Poolable) instance).getVerifier().setRecycled(true /*isRecycled*/);
  }
  resetter.reset(instance);
  return pool.release(instance);
}
}

它主要提供了acquire和release两个方法,在acquire方法中,FactoryPool首先尝试从缓存中获取,如果缓存中没有,再利用factory构造一个实例并返回,而这个factory就是在SafeKeyGenerator声明实现的时候传入的那个接口实现。相反,在release的时候,也是先设置回收对象被回收的标识,然后使用resetter来对回收对象进行一次reset。这些相关的实现很多都是基于接口并使用泛型,非常优雅,使FactoryPools扩展和复用非常方便。
绕了一圈后发现SafeKeyGenerator中的digestPool只实现了一个功能,即通过它的acquire方法肯定能得到一个PoolableDigestContainer对象,这个对象可能是从缓存中得到的,也可能是全新构造的,这些细节对外部调用类完全透明。
PoolableDigestContainer的实现很简单,主要用它来生成一个字符串的摘要,以便这个摘要能够用到后面的磁盘缓存当中作为key。

DiskCacheWriteLocker实现

DiskCacheWriteLocker通过名为locks的HashMap保存key和在key上的锁的映射关系。看看其内部主要的两个方法实现:

void acquire(Key key) {
    WriteLock writeLock;
    synchronized (this) {       // 为了防止在多线程情况下可能出现一个key构造出多个writeLock的情况,这里使用同步
      writeLock = locks.get(key);
      if (writeLock == null) {
        writeLock = writeLockPool.obtain();
        locks.put(key, writeLock);
      }
      writeLock.interestedThreads++;        //引用计数
    }

    writeLock.lock.lock();
  }

  /**
   * 注意释放锁和writelock被回收在这里内部实现的区别
   * @param key
   */
  void release(Key key) {
    WriteLock writeLock;
    synchronized (this) {
      writeLock = Preconditions.checkNotNull(locks.get(key));
      if (writeLock.interestedThreads < 1) {
        throw new IllegalStateException("Cannot release a lock that is not held"
            + ", key: " + key
            + ", interestedThreads: " + writeLock.interestedThreads);
      }

      writeLock.interestedThreads--;
      if (writeLock.interestedThreads == 0) {   // 引用计数为0,释放
        WriteLock removed = locks.remove(key);
        if (!removed.equals(writeLock)) {
          throw new IllegalStateException("Removed the wrong lock"
              + ", expected to remove: " + writeLock
              + ", but actually removed: " + removed
              + ", key: " + key);
        }
        writeLockPool.offer(removed);       // 回到pool中
      }
    }

    writeLock.lock.unlock();        // 开锁
  }

acquire方法中,首先到locks中查找有没有已经为这个key关联上的锁,如果有,增加起interestedThreads数量后就阻塞到锁的lock方法处,如果没有,则从writeLockPool中得到一个锁然后在locks建立它和这个key的关联并且加锁。
在release方法中,同样先找到和指定key关联的锁,然后interestedThreads减一,表示一个线程要释放锁了,如果interestedThreads减为0,则从locks中删除key的lock的关联关系并将lock释放到缓存,表示目前没有线程使用这个key,然后释放锁,如果此时还有其他线程阻塞在该锁的lock方法处,就会被随机唤醒并执行。
需要注意的是acquire和release方法中为了避免多线程环境下同时调用acquire或者release方法,或者二者交叉调用造成locks数据结构和writeLock.interestedThreads错乱的情况,使用了synchronized进行了同步。

DiskLruCacheWrapper缓存操作实现

分析完以上两个组成部分后,DiskLruCacheWrapper分析起来就简单了,先看主要源码:

  @Override
  public File get(Key key) {
    String safeKey = safeKeyGenerator.getSafeKey(key);
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      Log.v(TAG, "Get: Obtained: " + safeKey + " for for Key: " + key);
    }
    File result = null;
    try {
      // It is possible that the there will be a put in between these two gets. If so that shouldn't
      // be a problem because we will always put the same value at the same key so our input streams
      // will still represent the same data.
      final DiskLruCache.Value value = getDiskCache().get(safeKey);
      if (value != null) {
        result = value.getFile(0);
      }
    } catch (IOException e) {
      if (Log.isLoggable(TAG, Log.WARN)) {
        Log.w(TAG, "Unable to get from disk cache", e);
      }
    }
    return result;
  }

  @Override
  public void put(Key key, Writer writer) {
    // We want to make sure that puts block so that data is available when put completes. We may
    // actually not write any data if we find that data is written by the time we acquire the lock.
    writeLocker.acquire(key);
    try {
      String safeKey = safeKeyGenerator.getSafeKey(key);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Put: Obtained: " + safeKey + " for for Key: " + key);
      }
      try {
        // We assume we only need to put once, so if data was written while we were trying to get
        // the lock, we can simply abort.
        DiskLruCache diskCache = getDiskCache();
        Value current = diskCache.get(safeKey);     // 缓存中已经有对应key的内容了,不做更新,直接返回
        if (current != null) {
          return;
        }

        DiskLruCache.Editor editor = diskCache.edit(safeKey);
        if (editor == null) {
          throw new IllegalStateException("Had two simultaneous puts for: " + safeKey);
        }
        try {
          File file = editor.getFile(0);
          if (writer.write(file)) {
            editor.commit();
          }
        } finally {
          editor.abortUnlessCommitted();
        }
      } catch (IOException e) {
        if (Log.isLoggable(TAG, Log.WARN)) {
          Log.w(TAG, "Unable to put to disk cache", e);
        }
      }
    } finally {
      writeLocker.release(key);
    }
  }

在get方法中,首先通过safeKeyGenerator得到传入参数的唯一摘要字符串作为查询缓存的key,然后直接通过DiskLruCache进行查询即可。
put方法则稍微多一个步骤,即在直接操作缓存文件之前,需要通过writeLocker来保证在多线程环境下同一个时刻多个线程同时调用put方法只能有一个线程的修改能够成功。如果一个线程没有成功获取到锁,那么它会一直阻塞到writeLocker.acquire(key)处,直到其他线程对这个key对应的缓存文件修改完毕并释放锁为止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值