@linux1s1s
2016-07-19T11:19:35.000000Z
字数 6624
阅读 2665
Fresco
2016-07
转载 作者:Desmond
Fresco中一共有三级缓存,其中前两级内存缓存都存储在Java堆上,本地缓存存储在本地文件目录中。
Fresco中专门用于缓存键的接口,有这两种类实现了CacheKey
:
BitmapMemoryCacheKey
用于已解码的内存缓存键,会对Uri字符串、缩放尺寸、解码参数、PostProcessor等关键参数进行hashCode作为唯一标识;SimpleCacheKey
普通的缓存键实现,使用传入字符串的hashCode作为唯一标识,所以需要保证相同键传入字符串相同。已解码的内存缓存(BitmapMemoryCache)与未解码的内存缓存(EncodedMemoryCache)实现唯一区别就是已解码内存缓存的数据是CloseableReference<CloseableBitmap>而未解码内存缓存的数据是CloseableReference。即他们的实现方式一样,区别仅仅在于资源的测量与释放方式不同。它们使用ValueDescriptor
来描述不同资源的数据大小,使用不同的ResourceReleaser
来释放资源。
内存缓存中使用了LRU(Least Recent Used)来提高缓存功能,我们来看一下Fresco中实现它的逻辑。
在CountingLruMap
中使用了LinkedHashMap
作为数据存储载体,这个HashMap很特别,它内部有一个双向链表,在做查找操作的时候,从最先插入的单位开始查询。这就提供了一种好处:它能够很快地删除掉最早插入的单位!所以它非常适合LRU缓存来使用。
但是由于在LinkedHashMap
中重复插入相同单位并不会影响链表顺序,所以要用CountingLruMap
将它包装,我们来看看它put对象时的逻辑:
public synchronized V put(K key, V value) {
V oldValue = mMap.remove(key);
mSizeInBytes -= getValueSizeInBytes(oldValue);
mMap.put(key, value);
mSizeInBytes += getValueSizeInBytes(value);
return oldValue;
}
它会先将要插入的对象remove掉,然后重新插入该对象!由此来保证最新加入的对象处于正确地插入顺序中。同时在mSizeBytes
中更新现在缓存池中所有对象的字节数。
在CountingHruMap
中还有以下几个重要函数:
get(Key)
查找对象,如有则返回;remove(Key)
删除对象并返回它;getFirstkey()
获取最早插入的对象;getCount()
获取已经缓存的对象数;getSizeInBytes()
获取缓存池中已经使用的大小。Fresco中实现具体内存缓存的类是CountingMemoryCache
,它内部维持着几个重要参数:
ExclusiveEntries
存储着未被使用的对象的CountingLruMap
;CachedEntries
存储着所有对象的CountingLruMap
;MemoryCacheParams
存储着最大缓存对象数量、缓存池大小等参数、PARAMS_INTERCHECK_INTERVAL_MS
检查缓存参数变化的事件间隔:5分钟;它使用一个内部类Entry
来封装缓存对象,除了记录缓存键、缓存对象之外,它还记录着该对象的引用数量(clientCount
)及是否被缓存追踪(isOrphan
)。注意:每个缓存对象只有满足clientCount
为0并且isOrphan
为true时才可以被释放。(可从referenceToClose
函数中看出此逻辑)
我们来看看它缓存对象的逻辑:
public CloseableReference<V> cache(final K key, final CloseableReference<V> valueRef) {
//检查参数
maybeUpdateCacheParams();
CloseableReference<V> oldRefToClose = null;
CloseableReference<V> clientRef = null;
synchronized (this) {
mExclusiveEntries.remove(key);
Entry<K, V> oldEntry = mCachedEntries.remove(key);
if (oldEntry != null) {
makeOrphan(oldEntry);
oldRefToClose = referenceToClose(oldEntry);
}
if (canCacheNewValue(valueRef.get())) {
Entry<K, V> newEntry = Entry.of(key, valueRef);
mCachedEntries.put(key, newEntry);
clientRef = newClientReference(newEntry);
}
}
CloseableReference.closeSafely(oldRefToClose);
maybeEvictEntries();
return clientRef;
}
我们看到它会做几件事:
maybeUpdateCacheParams
中检查是否需要更新缓存参数;clientCount
,并将它加到ExclusiveEntries
中,如果可释放则直接释放资源。最后会调用maybeEvectEntries
函数,判断是否需要释放资源,它的逻辑是:当ExclusiveEntries中已经缓存的对象超过缓存池的最大容纳对象或者已经超过了缓存池容量时,删除ExclusiveEntries
中最早插入的对象,直到ExclusiveEntries缓存对象小于最大容纳对象并且在缓存池容量以内。实际上这个函数会在大部分的缓存操作中出现,保证每次操作结束后缓存处于一个健康的状态。
Fresco使用InstrumentedMemoryCache
包装了CountingMemoryCache
,主要增加的功能就是提供了MemoryCacheTracker
,会在缓存命中或未命中时提供回调函数,供使用者实现自定义功能。
可以通过ImagePipelineConfig的以下两个函数来实现内存缓存参数部分自定义:
setBitmapMemoryCacheParamsSupplier(Supplier<MemoryCacheParams> bitmapMemoryCacheParamsSupplier)
setEncodedMemoryCacheParamsSupplier(Supplier<MemoryCacheParams> encodedMemoryCacheParamsSupplier)
这两个函数都需要提供MemoryCacheParams
的Supplier,使用者可以自定义ImagePipelineConfig之后在初始化中应用它。
由于文件缓存是直接存储在磁盘上的,所以它的实现方式与内存缓存不同,而且带有缓冲区域,所以更加复杂。我总结它一共有三层内容:文件存储层,文件缓存层,缓冲缓存层。
文件缓存都是将实际的文件存储在存储设备中,Fresco的文件存储有两种格式的文件:
.cnt
实际存储的内容文件.tmp
临时文件Fresco中定义了BinaryResource
来封装文件对象,你可以通过它获取文件的输入流、字节码等。此外,Fresco定义了每个文件的唯一描述符,此描述符由CacheKey
的.toString()
导出字符串的SHA-1哈希码再经过Base64加密得出。
文件存储层有两个重要工具:DefaultDiskStorageSupplier与DefaultDiskStorage。
DefaultDiskStorageSupplier
可以用来创建缓存文件目录及获取到对应的DefaultDiskStorage
,是一个典型的Supplier。
DefaultDiskStorage
是文件操作者,它实现了DiskStorage
接口,负责Fresco中存取文件的逻辑与实现,具体有以下功能:
getEntries()
获取缓存目录下所有文件的Entry
(Entry对象中存储着文件的BinaryResource、TimeStamp及大小)。getResource(String resourceId, Object debugInfo)
获取描述符指向的文件,更新时间戳;contains(String resourceId, Object debugInfo)
检查是否包含描述符指向的文件;createTemporary(String resourceId, Object debugInfo)
以指定描述符创建临时文件;commit(String resourceId, BinaryResource temporary, Object debugInfo)
将BinaryResource
写入描述符指向的文件中,更新时间戳;remove(String resourceId)
删除描述符指向的文件,正常返回被删除文件的大小,文件不存在则返回0,其他返回-1。DiskStorageCache是Fresco实现文件缓存的主要类,在文件缓存中也使用了相应的LRU技术提高缓存效率,我们来看看它是怎么实现的。
在evictAboveSize中我们可以看到所使用的LRU逻辑:
private void evictAboveSize(
long desiredSize,
CacheEventListener.EvictionReason reason) throws IOException {
DiskStorage storage = mStorageSupplier.get();
Collection<DiskStorage.Entry> entries;
try {
entries = getSortedEntries(storage.getEntries());
} catch (IOException ioe) {
//异常捕捉
}
//要删除的数据量
long deleteSize = mCacheStats.getSize() - desiredSize;
//记录删除数据数量
int itemCount = 0;
//记录删除数据的大小
long sumItemSizes = 0L;
for (DiskStorage.Entry entry : entries) {
if (sumItemSizes > (deleteSize)) {
break;
}
long deletedSize = storage.remove(entry);
if (deletedSize > 0) {
itemCount++;
sumItemSizes += deletedSize;
}
}
mCacheStats.increment(-sumItemSizes, -itemCount);
storage.purgeUnexpectedResources();
reportEviction(reason, itemCount, sumItemSizes);
}
我们可以看到在这个函数中它获取两个输入:期望达到的剩余容量及缓存事件监听者。接下来以以下顺序进行操作:
Entry
的Collection,以它们被访问时间进行排序,最近被访问的Entry在后面;在maybeEvictFilesInCacheDir
函数中我们可以看到当缓存过载时会以缓存容量的90%为目标进行清理。
在文件缓存中维持一个对象mLock
,该对象就是为了让各个操作保持同步。我们来看一下DiskStorageCache
中的几个重要底层操作函数及它们的同步情况:
getResource(final CacheKey key)
同步操作,从指定CacheKey获取文件描述符,如果存在则返回它的BinaryResource
;createTemporaryResource(String resourceId, CacheKey key)
非同步操作,检查是否需要清理缓存,如果需要则进行清理,之后创建临时文件并返回它的BinaryResource;deleteTemporaryResource(BinaryResource temporaryResource)
非同步操作,删除BinaryResource指向的文件;BinaryResource commitResource(String resourceId, CacheKey key, BinaryResource temporary)
同步操作,将temporary写入文件描述符指向的文件;这么做主要是要保证写入最终缓存文件的原子性,我们可以看它提供的写入缓存函数:
public BinaryResource insert(CacheKey key, WriterCallback callback) throws IOException {
mCacheEventListener.onWriteAttempt();
final String resourceId = getResourceId(key);
try {
BinaryResource temporary = createTemporaryResource(resourceId, key);
try {
mStorageSupplier.get().updateResource(resourceId, temporary, callback, key);
return commitResource(resourceId, key, temporary);
} finally {
deleteTemporaryResource(temporary);
}
} catch (IOException ioe) {
//异常处理
}
}
我们看到它在插入的时候会首先创建临时文件(此时多个任务可以并行地操作),而在将temporary数据commit到描述符指定的文件中是同步操作。
注意:这个函数并没有直接提供要写入的数据,而是在updateResource
函数中通过WriterCallback
实现的自定义写入函数将数据写到temporary中,会在缓冲缓存层中进一步解释。
此外,它的删除资源操作remove
函数也是同步的。
缓冲缓存层BufferedDiskCache将缓存层进行包装,它主要多了三个功能:
writeToDiskCache
中可以看出它提供的WriterCallback
将要写入的EncodedImage
转码成输入流;可以通过调用ImagePipelineConfig.setMainDiskCacheConfig(DiskCacheConfig mainDiskCacheConfig)
设置文件缓存。
DiskCacheConfig使用Builder模式创建,它可以自定义缓存路径、缓存文件夹名称、缓存池大小等,具体自定义内容可以参见DiskCacheConfig。