Fresco源码分析五

内存缓存的实现:

CacheKey:用于缓存键的接口,替代Object,所以要实现equals()和hashCode()方法,而toString()方法用于单元测试和调试。
SimpleCacheKey:CacheKey的简单实现,使用传入的字符串key值的hashCode作为唯一标识。
BitmapMemoryCacheKey:已解码的内存缓存键,用URI字符串的hashCode,调整大小尺寸,是否自动旋转,图片解码选项,后处理字符串等参数最为唯一标识。

MemoryCache:用于缓存的接口,包含一对键值,并提供增删改查四大基本方法,cache(),get(),removeAll(),contains()。
InstrumentedMemoryCache:带有MemoryCacheTracker的MemoryCache,对MemoryCache做简单的包装,使其可以分别在加入到缓存,查找命中,查找未命中的时候回调MemoryCacheTracker的onCachePut(),onCacheHit(),onCacheMiss()方法。

CountingMemoryCache:该类实现具体 的内存缓存,实现了MemoryCache和 MemoryTrimmable接口。MemoryTrimmable是在内存不足时,控制释放内存部分资源的接口,只包含trim()一个方法,该方法参数类型是枚举类MemoryTrimType,MemoryTrimType提供一个参数suggestedTrimRatio,该参数是根据APP的不同情况建议释放内存的比率。

CountingMemoryCache主要包括四个重要的成员变量和增删改查四个方法的具体实现。
四个重要的成员变量分别为:
Entry包装了缓存键(K)和缓存值(CloseableReference),对象被引用的数量(clientCount)和是否被缓存追踪(isOrphan),以及EntryStateObserver接口,该接口通过onExclusivityChanged(K key, boolean isExclusive)函数在回收队列状态改变的时候通过回调控制该key对应的对象是否可以被重用(isExclusive)。

1
2
3
4
5
6
7
8
//存储未被使用的对象,已经准备好被回收,但是还没有被回收
final CountingLruMap<K, Entry<K, V>> mExclusiveEntries;
//存储全部的缓存对象,包括未被使用的对象
final CountingLruMap<K, Entry<K, V>> mCachedEntries;
//该接口通过唯一的方法getSizeInBytes()返回V值的大小,单位是byte
private final ValueDescriptor<V> mValueDescriptor;
//该接口通过唯一的方法getTrimRatio()返回MemoryTrimType的suggestedTrimRatio
private final CacheTrimStrategy mCacheTrimStrategy;

CountingLruMap:LinkedHashMap,增删改查方法。
增删改查方法都是对mExclusiveEntries和mCachedEntries的相应操作。
其中:
maybeUpdateCacheParams():检查是否需要更新缓存参数,时间间隔是5分钟。
所谓的缓存参数是(MemoryCacheParams):
缓存最大值(maxCacheSize) 单位byte,
缓存最多的对象个数(maxCacheEntries),
回收队列最大值(maxEvictionQueueSize), 单位byte
回收队列最多的对象个数(maxEvictionQueueEntries),
单个缓存对象的最大值(maxCacheEntrySize)。

uptimeMillis()返回的是系统从启动到当前处于非休眠期的时间。
elapsedRealTime()返回的是系统从启动到现在的时间。

maybeEvictEntries():如果超过了回收队列的最大值或者是回收队列的对象的最多个数,就从回收队列移除第一个对象,将该对象的isOrphan设置为true,并释放该对象。

缓存操作的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public CloseableReference<V> cache(
final K key,
final CloseableReference<V> valueRef,
final EntryStateObserver<K> observer) {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(valueRef);

maybeUpdateCacheParams();

Entry<K, V> oldExclusive;
CloseableReference<V> oldRefToClose = null;
CloseableReference<V> clientRef = null;
synchronized (this) {
// remove the old item (if any) as it is stale now
oldExclusive = 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, observer);
mCachedEntries.put(key, newEntry);
clientRef = newClientReference(newEntry);
}
}
CloseableReference.closeSafely(oldRefToClose);
maybeNotifyExclusiveEntryRemoval(oldExclusive);

maybeEvictEntries();
return clientRef;
}

所以,首先要先检查缓存参数是否要更新,因为缓存对象之后要改变mExclusiveEntries和mCachedEntries的大小,要先查看两个队列大小和对象个数的设置。
然后,从两个队列分别取出KEY值对应的Entry对象。如果对象不为NULL就将其移除并尝试释放。
检查mCachedEntries是否还有空间,如果有就插入。并将这个对象包装成CloseableReference对象返回。

1
2
3
4
5
6
7
8
9
10
11
private synchronized CloseableReference<V> newClientReference(final Entry<K, V> entry) {
increaseClientCount(entry);
return CloseableReference.of(
entry.valueRef.get(),
new ResourceReleaser<V>() {
@Override
public void release(V unused) {
releaseClientReference(entry);
}
});
}

该对象自增引用计数并具有自动释放资源的方法。
最后,调用maybeEvictEntries()检查回收队列的状态并回调observer.onExclusivityChanged()。

1
2
3
4
5
private static <K, V> void maybeNotifyExclusiveEntryRemoval(@Nullable Entry<K, V> entry) {
if (entry != null && entry.observer != null) {
entry.observer.onExclusivityChanged(entry.key, false);
}
}

mCachedEntries和mExclusiveEntries释放资源的逻辑分别是:
mCachedEntries:entry.isOrphan && entry.clientCount == 0
mExclusiveEntries:(mExclusiveEntries.getCount() > count || mExclusiveEntries.getSizeInBytes() > size

BitmapCountingMemoryCacheFactory:提供用于已解码缓存的CountingMemoryCache的工厂。
get()方法返回CountingMemoryCache
EncodedCountingMemoryCacheFactory:提供用于未解码缓存的CountingMemoryCache的工厂。
get()方法返回CountingMemoryCache

BitmapMemoryCacheFactory:提供已解码的内存缓存(BitmapMemoryCache)的工厂。
EncodedMemoryCacheFactory:提供未解码的内存缓存(EncodedMemoryCache)的工厂。

两者的区别仅仅在于获取资源大小(ValueDescriptor)的方式和释放资源的策略(MemoryTrimmableRegistry)不同。

文件缓存的实现:
DiskStorage:负责文件存取的接口,包含一些存取文件的基本方法并通过内部类DiskDumpInfo保存了文件的一些信息。
主要方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//通过resourceId获取到文件
BinaryResource getResource(String resourceId, Object debugInfo) throws IOException;
//通过resourceId查询是否包含文件
boolean contains(String resourceId, Object debugInfo) throws IOException;
//通过resourceId查询是否包含文件,如果存在更新最近访问时间
boolean touch(String resourceId, Object debugInfo) throws IOException;
//创建一个临时文件
BinaryResource createTemporary(String resourceId, Object debugInfo) throws IOException;
//提交createTemporary()方法创建的临时文件
BinaryResource commit( String resourceId,BinaryResource temporary,Object debugInfo)throws IOException;
//获取缓存目录下所有文件的Entry,Entry内部封装了文件的时间戳,大小,getResource()方法
Collection<Entry> getEntries() throws IOException;
//删除文件
long remove(String resourceId) throws IOException;
//获取文件Entry的信息
DiskDumpInfo getDumpInfo() throws IOException;

首先,BinaryResource接口用于封装文件对象,
它包含获取该文件输入流的方法(InputStream openStream()),以字节数组的方式读取该文件的方法(byte[] read())和获取该文件大小的方法(size())。
它的实现类有两个,ByteArrayBinaryResource类是BinaryResource的字节数组形式,通过文件的字节数组构造,同时openStream()方法返回ByteArrayInputStream(),ByteArrayInputStream()将一个字节数组当作流输入的来源。FileBinaryResource类是BinaryResource的File形式,通过File构造,同时openStream()方法返回文件输入流FileInputStream()。

其次,以上这些和“增删改查”相关的方法都是通过resourceId“实名存取”的。
DefaultDiskStorage是DiskStorage的实现类。

1
2
private static final String CONTENT_FILE_EXTENSION = ".cnt";
private static final String TEMP_FILE_EXTENSION = ".tmp";

其中,.cnt是实际存储的内容文件, .tmp是临时文件。

1
2
3
4
5
6
7
8
public FileBinaryResource getResource(String resourceId, Object debugInfo) {
final File file = getContentFileFor(resourceId);
if (file.exists()) {
file.setLastModified(mClock.now());
return FileBinaryResource.createOrNull(file);
}
return null;
}

通过resourceId的哈希码创建.cnt文件。

1
2
3
4
5
6
File getContentFileFor(String resourceId) {
FileInfo fileInfo = new FileInfo(FileType.CONTENT, resourceId);
//获取用于缓存文件的目录
File parent = getSubdirectory(fileInfo.resourceId);
return fileInfo.toFile(parent);
}

1
2
3
4
private File getSubdirectory(String resourceId) {
String subdirectory = String.valueOf(Math.abs(resourceId.hashCode() % SHARDING_BUCKET_COUNT));
return new File(mVersionDirectory, subdirectory);
}

// mVersionDirectory’s name identifies:
// - the cache structure’s version (sharded)
// - the content’s version (version value)
// if structure changes, prefix will change… if content changes version will be different
// the ideal would be asking mSharding its name, but it’s created receiving the directory

DiskStorageCache:DiskStorageCache实现了FileCache接口和DiskTrimmable接口,是建立在DisKStorage之上的文件缓存(DisKCache),是真正通过CacheKey存取的缓存类。
FileCache:为了和DiskStorage实现功能的对接,其含有类似的“增删改查”方法,如getResource(),insert(),remove(),当然这些方法的参数都是CacheKey类型。
DiskTrimmable:其中包含两个回调方法,
1)trimToMinimum() disk空间剩下很少一部分的时候回调
2)trimToNothing() disk空间几乎没有剩余,app几乎要crash的时候回调

ResourceId是key.toString().getBytes(“UTF-8”)通过SHA-1加密再经过Base64编码获得的。

1
2
3
4
5
6
7
8
String getResourceId(final CacheKey key) {
try {
return SecureHashUtil.makeSHA1HashBase64(key.toString().getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
// This should never happen. All VMs support UTF-8
throw new RuntimeException(e);
}
}

其主要的getResource(),insert(),remove()方法均是通过DiskStorageSupplier的get()方法获取到DisKStorage然后再调用相应的方法执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void trimToMinimum() {
synchronized (mLock) {
maybeUpdateFileCacheSize();
long cacheSize = mCacheStats.getSize();
if (mCacheSizeLimitMinimum <= 0 || cacheSize <= 0 || cacheSize < mCacheSizeLimitMinimum) {
return;
}
double trimRatio = 1 - (double) mCacheSizeLimitMinimum / (double) cacheSize;
if (trimRatio > TRIMMING_LOWER_BOUND) {
trimBy(trimRatio);
}
}
}

maybeUpdateFileCacheSize()方法重新计算缓存的大小,将缓存的大小(Byte)和缓存内的Item数量保存在CacheStats类的实例中。
mCacheSizeLimitMinimum表示期待留下的空间的大小。(总空间大小-mCacheSizeLimitMinimum)即是将要回收的空间大小。
trimToMinimum()调用trimBy(trimRatio),trimRatio是回收空间占总空间的比例。
trimBy()中进入evictAboveSize()函数:

1
2
3
4
long cacheSize = mCacheStats.getSize();
long newMaxBytesInFiles = cacheSize - (long) (trimRatio * cacheSize);
evictAboveSize(newMaxBytesInFiles,
CacheEventListener.EvictionReason.CACHE_MANAGER_TRIMMED);

getSortedEntries()方法将缓存目录下的所有Entry按着访问时间进行排序,最近访问的图片放在数组的最后。

1
2
3
4
5
6
7
8
private Collection<DiskStorage.Entry> getSortedEntries(
Collection<DiskStorage.Entry> allEntries) {
final ArrayList<DiskStorage.Entry> entriesList = new ArrayList<>(allEntries);
final long threshold =
mClock.now() + DiskStorageCache.FUTURE_TIMESTAMP_THRESHOLD_MS;
Collections.sort(entriesList, new TimestampComparator(threshold));
return entriesList;
}

然后,从前向后依次删除,直到缓存的空间达到期待留下的空间的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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) {
mCacheErrorLogger.logError(
CacheErrorLogger.CacheErrorCategory.EVICTION,
TAG,
"evictAboveSize: " + ioe.getMessage(),
ioe);
throw 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);
}

而trimToNothing()方法则直接调用clearAll()方法,具体实现不再追踪。
主要代码如下:

1
2
mStorageSupplier.get().clearAll();
mCacheStats.reset();

BufferedDiskCache:文件缓存缓冲区是在DiskStorageCache之上的包装,提供了get/put方法用于Disk-Cache的read/writes。
主要有四个方法:
1)Task contains(final CacheKey key)方法:
如果在staging area内查找成功,就直接返回。

1
2
3
4
5
6
final EncodedImage pinnedImage = mStagingArea.get(key);
if (pinnedImage != null) {
pinnedImage.close();
mImageCacheStatsTracker.onStagingAreaHit();
return Task.forResult(true);
}

如果,没有查找到就在后台线程中去disk-cache中查找,mFileCache.hasKey(key)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
return Task.call(
new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
EncodedImage result = mStagingArea.get(key);
if (result != null) {
result.close();
mImageCacheStatsTracker.onStagingAreaHit();
return true;
} else {
mImageCacheStatsTracker.onStagingAreaMiss();
try {
return mFileCache.hasKey(key);
} catch (Exception exception) {
return false;
}
}
}
},
mReadExecutor);

StagingArea:是存储将要写入disk-cache但是还没有被写入disk-cache的数据的区域。StagingArea内部维持一个HashMap数据结构,用以记录图片的CacheKey和EncodedImage(未解码图片)的对应关系。
2)3)get()和put()方法也都是先在StagingArea内查找。
get()方法的主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try {
final PooledByteBuffer buffer = readFromDiskCache(key);
CloseableReference<PooledByteBuffer> ref = CloseableReference.of(buffer);
try {result = new EncodedImage(ref);}
finally {CloseableReference.closeSafely(ref);}
} catch (Exception exception) {
return null;
}
if (Thread.interrupted()) {
if (result != null) {
result.close();
}throw new InterruptedException();
} else {
return result;
}

readFromDiskCache()方法主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final BinaryResource diskCacheResource = mFileCache.getResource(key);
if (diskCacheResource == null) {
mImageCacheStatsTracker.onDiskCacheMiss();
return null;
} else {
mImageCacheStatsTracker.onDiskCacheHit();
}
PooledByteBuffer byteBuffer;
final InputStream is = diskCacheResource.openStream();
try {
byteBuffer = mPooledByteBufferFactory.newByteBuffer(
is,(int)diskCacheResource.size());
} finally {
is.close();
}
return byteBuffer;

可见具体过程是:
BufferedDiskCache.get(key) EncodedImage 加入StagingArea缓冲,操作读写
DiskStorageCache.getResource(key) BinaryResource LRU
DefaultDiskStorage.getResource(getResourceId(key)) File 文件存储
所以,最开始是从Disk具体的目录读取出File,然后封装成BinaryResource形式,通过BinaryResource的openStream()转换成输入流,再转换成PooledByteBuffer,最后转换成EncodedImage。

put()方法的主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
mStagingArea.put(key, encodedImage);
mWriteExecutor.execute(
new Runnable() {
@Override
public void run() {
try {
writeToDiskCache(key, finalEncodedImage);
} finally {
mStagingArea.remove(key, finalEncodedImage);
EncodedImage.closeSafely(finalEncodedImage);
}
}
});

将encodedImage.getInputStream()拷贝到os。

1
2
3
4
5
6
7
mFileCache.insert(
key, new WriterCallback() {
@Override
public void write(OutputStream os) throws IOException {
mPooledByteStreams.copy(encodedImage.getInputStream(), os);
}
}

所以,现在回过头来看DiskStorageCache的insert()代码:

1
2
3
4
5
6
7
8
9
10
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);
}
}

先创建一个临时文件,通过updateResource()的WriterCallback回调向临时文件temporary做写入的操作,然后提交临时文件.tmp使其变成.cnt文件,最后再删除掉临时文件。

updateResource()主要代码如下:

1
2
3
fileStream = new FileOutputStream(file);
CountingOutputStream countingStream = new CountingOutputStream(fileStream);
callback.write(countingStream);

从类型为BinaryResource的临时文件中提取出File类型的文件file转换成FileOutputStream

文章目录