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对应的缓存文件修改完毕并释放锁为止。