Fresco框架(二)-- 内存缓存
在上一篇追踪源码,梳理了一下Fresco加载中涉及到的一些对象,以及在这个架构中如何完成请求。然后分析了网络请求的加载过程,分析了ProducerSequence的组成部分,一期完整的请求流程需要经过如下对象和工作:
-
BitmapMemoryCacheGetProducer,图片缓存读取
-
ThreadHandoffProducer,线程切换
-
BitmapMemoryCacheKeyMultiplexProducer,多路复用,相同请求进行合并
-
BitmapMemoryCacheProducer,图片缓存
-
ResizeAndRotateProducer,图片调整
-
AddImageTransformMetaDataProducer,元数据解码
-
EncodedCacheKeyMultiplexProducer,元数据多路复用
-
EncodedMemoryCacheProducer,元数据缓存
-
DiskCacheReadProducer,磁盘缓存读取
-
DiskCacheWriteProducer,磁盘缓存写入
-
WebpTranscodeProducer,webp转码
-
NetworkFetchProducer,网络请求
其中总共具有三层缓存
-
第一层Bitmap缓存,Bitmap缓存存储Bitmap对象,这些Bitmap对象可以立即用来显示,在线程切换之前就读缓存,缓存在内存当中,在后台会被清掉,Bitmap相对于元数据会大很多,参考之前的Bitmap相关知识
-
第二层元数据缓存,元数据缓存存储原始压缩格式图片如png、jpg,这些缓存在使用时需要先解码成bitmap,使用会再次缓存到第一层缓存,缓存在内存中,在后台会被清掉
-
第三层元数据缓存,与第二层的缓存数据完全一致,使用时需要解码,使用会再次缓存到第一层和第二层缓存中,缓存在磁盘中,在后台不回被清除
BitmapMemoryCacheProducer
内存缓存是一种用于存储图片数据的临时存储空间,可以快速地访问和加载图片资源,提高图片加载的效率和性能。内存缓存通常存储在RAM中,因此可以快速地读取和写入数据。
介绍
Fresco的缓存架构中,前两层都是使用的内存缓存,分别针对的是Bitmap数据和元数据:
-
Bitmap缓存涉及两个Producer,BitmapMemoryCacheGetProducer和BitmapMemoryCacheProducer。
- BitmapMemoryCacheGetProducer继承自BitmapMemoryCacheProducer,禁止了其写缓存的能力。所以逻辑还是在BitmapMemoryCacheProducer中。
- 当图片从网络或本地加载后,经过解码生成位图后,BitmapMemoryCacheProducer会将这些位图数据存储到内存缓存中。下次再次加载相同的图片时,可以直接从内存缓存中读取位图数据,避免重新解码,提高图片加载的速度和效率。
-
元数据缓存EncodedMemoryCacheProducer,负责将原始的编码数据存储到编码内存缓存中。当图片从网络或本地加载后,未经过解码的编码数据会被EncodedMemoryCacheProducer存储到编码内存缓存中。这样在需要重新加载图片时,可以直接从编码内存缓存中读取原始的编码数据,再解码生成位图,避免重新下载图片,提高加载速度。
从功能上可以看出,BitmapMemoryCacheProducer和EncodedMemoryCacheProducer除了针对的对象不同之外,逻辑是完全相同的。
代码流程
- 当BitmapMemoryCacheProducer的produceResults方法被调用时,首先从ProducerContext中获取到对应的CacheKey。
- 接着通过CacheKey从内存缓存中查找是否有对应的位图数据。如果内存缓存中有对应的位图数据,则直接将数据返回给Consumer,并关闭CloseableReference。
- 如果内存缓存中没有对应的位图数据,则调用下一个生产者(mInputProducer)的produceResults方法,同时将一个包装过的Consumer传入其中。
- 包装过的Consumer当从下一个生产者获取到新的结果时,将结果存储到内存缓存中,并继续传递结果给原始的Consumer。
其中缓存获取和存储分别是通过MemoryCache的get和put方法实现。
public class BitmapMemoryCacheProducer implements Producer<CloseableReference<CloseableImage>> {
private final Producer<CloseableReference<CloseableImage>> mInputProducer;
private final MemoryCache<CacheKey, CloseableImage> mMemoryCache;
public BitmapMemoryCacheProducer(
Producer<CloseableReference<CloseableImage>> inputProducer,
MemoryCache<CacheKey, CloseableImage> memoryCache) {
mInputProducer = Preconditions.checkNotNull(inputProducer);
mMemoryCache = Preconditions.checkNotNull(memoryCache);
}
@Override
public void produceResults(Consumer<CloseableReference<CloseableImage>> consumer, ProducerContext context) {
//从ProducerContext中获取到对应的CacheKey
final ImageRequest imageRequest = producerContext.getImageRequest();
final Object callerContext = producerContext.getCallerContext();
final CacheKey cacheKey = mCacheKeyFactory.getBitmapCacheKey(imageRequest, callerContext);
// 从内存缓存中查找是否有对应的位图数据
CloseableReference<CloseableImage> closeableImage = mMemoryCache.get(cacheKey);
if (closeableImage != null) {
// 如果内存缓存中有对应的位图数据,则直接返回给consumer
consumer.onProgressUpdate(1f);
consumer.onNewResult(closeableImage, true);
closeableImage.close();
} else {
// 如果内存缓存中没有对应的位图数据,则继续向下一个生产者请求数据
Consumer<CloseableReference<CloseableImage>> wrappedConsumer =
wrapConsumer(
consumer, cacheKey, producerContext.getImageRequest().isMemoryCacheEnabled());
mInputProducer.produceResults(wrappedConsumer, context);
}
}
private Consumer<CloseableReference<CloseableImage>> wrapConsumer(final Consumer<CloseableReference<CloseableImage>> consumer, final CacheKey cacheKey) {
return new DelegatingConsumer<CloseableReference<CloseableImage>, CloseableReference<CloseableImage>>(consumer) {
@Override
public void onNewResultImpl(CloseableReference<CloseableImage> result, boolean isLast) {
// 当从下一个生产者获取到新的结果时,将结果存储到内存缓存中
if (isLast) {
if (result != null) {
newCachedResult = mMemoryCache.cache(cacheKey, newResult);
}
}
// 继续传递结果给Consumer
getConsumer()
.onNewResult((newCachedResult != null) ? newCachedResult : newResult, status);
}
};
}
}
LruCountingMemoryCache
介绍
MemoryCache是Fresco中非常重要的一个组件,它能够有效地管理内存中的图片缓存,提高图片加载的效率和性能。MemoryCache支持配置不同的缓存策略,包括缓存的大小限制、缓存的有效期、缓存的清理策略等。可以根据自己的需求和场景来调整MemoryCache的配置。
LruCountingMemoryCache是MemoryCache一个主要实现。是一个结合了LRU算法和计数功能的内存缓存类,LRU算法会根据最近访问的顺序来淘汰最少使用的数据,以保持缓存大小在一定范围内。
存储方式和对象
-
存储方式
- mExclusiveEntries存储的是那些不被任何客户端使用的缓存条目,这些是可被清理的、空闲的缓存条目,因此这些条目可以被驱逐出缓存,当一个条目不再被使用时,会被移动到mExclusiveEntries
- mCachedEntries则是存储所有缓存条目的地方,包括那些被标记为独占的条目和普通的缓存条目。
final CountingLruMap<K, Entry<K, V>> mExclusiveEntries;
final CountingLruMap<K, Entry<K, V>> mCachedEntries;
-
存储对象
- key是条目的键,用于唯一标识该条目在缓存中的位置。
- valueRef是一个CloseableReference类型的成员变量,用于存储条目对应的值的引用。
- clientCount表示引用该条目值的客户端数量,即有多少个客户端正在使用这个值。
- isOrphan表示该条目是否孤立,孤立的条目意味着这个条目不再被缓存管理器追踪。
class Entry<K, V> {
public final K key;
public final CloseableReference<V> valueRef;
public int clientCount;
public boolean isOrphan;
}
读取
从缓存中获取指定key对应的值,如果有缓存返回一个引用,如果没有缓存返回空。
- 从mExclusiveEntries中移除对应的Entry。
- 从mCachedEntries中取对应的Entry。
- 如果获取到了Entry,则调用newClientReference()创建一个新的CloseableReference引用。
- maybeUpdateCacheParams()和maybeEvictEntries(),第一个可能会根据缓存的大小、存储策略等因素来更新缓存的相关参数。第二个可能逐出不必要的缓存项,因为上一步可能更改了缓存参数,导致需要重新计算。
@Nullable
public CloseableReference<V> get(final K key) {
Entry<K, V> oldExclusive;
CloseableReference<V> clientRef = null;
synchronized (this) {
oldExclusive = mExclusiveEntries.remove(key);
Entry<K, V> entry = mCachedEntries.get(key);
if (entry != null) {
clientRef = newClientReference(entry);
}
}
maybeUpdateCacheParams();
maybeEvictEntries();
return clientRef;
}
引用
读取缓存时返回的对象是实际对象的一个全新的引用,Entry存储的是外面传入的CloseableReference引用,而从缓存中取条目的时候,会使用这个CloseableReference引用的对象创建一个新的CloseableReference引用,供给外面的调用方单独使用,使用完成后对引用进行释放,通过这样实现了一对多的管理方式。
-
newClientReference方法用于创建一个新的引用
- 首先通过increaseClientCount()增加计数,表示有一个新的引用该条目值。
- 然后创建一个CloseableReference,管理对条目值的引用,并在引用不再需要时释放资源。
- 当引用需要释放时,会调用releaseClientReference()方法释放引用。
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);
}
});
}
-
releaseClientReference方法用于释放引用,
- 如减少客户端计数、可能将条目添加到独占集合、关闭旧的引用等。
- 首先,减少了条目的客户端计数,表示有一个客户端不再引用该条目值。
- maybeAddToExclusives(),如果计数降为0但是没有孤立,表示条目不再被使用时,会被移动到mExclusiveEntries。
- referenceToClose(),计数降为0而且已经孤立,发生在缓存已经因为其他情况清除掉的情况下,并且当前释放的已经是条目最后一个引用,在这里将它释放掉。
- maybeUpdateCacheParams()和maybeEvictEntries(),第一个可能会根据缓存的大小、存储策略等因素来更新缓存的相关参数。第二个可能逐出不必要的缓存项,因为上一步可能更改了缓存参数,导致需要重新计算。
private void releaseClientReference(final Entry<K, V> entry) {
Preconditions.checkNotNull(entry);
boolean isExclusiveAdded;
CloseableReference<V> oldRefToClose;
synchronized (this) {
decreaseClientCount(entry);
isExclusiveAdded = maybeAddToExclusives(entry);
oldRefToClose = referenceToClose(entry);
}
CloseableReference.closeSafely(oldRefToClose);
maybeUpdateCacheParams();
maybeEvictEntries();
}
存储
将键值对缓存到内存中。
-
maybeUpdateCacheParams(),根据缓存的大小、存储策略等因素来更新缓存的相关参数。这个跟之前几个方法有区别,放到了最前面来。
-
第二步去重操作,查找mExclusiveEntries和mCachedEntries中是否已经存储了相同的key,如果有就把它清除掉,这里一部分操作在同步块内,一部分在外面,这是为了避免潜在的死锁情况,因为在调用close方法时可能会涉及到其他线程或者资源的释放操作,如果在持有锁的情况下调用这个方法,可能会导致死锁。下面是具体流程:
- mExclusiveEntries.remove和mCachedEntries.remove找出对应的缓存
- 如果确实从缓存中查到了值,makeOrphan()标记条目已经孤立,eferenceToClose(),计数为0而且已经孤立,在这里将它释放掉。如果计数不为0,说明还有使用方,等待所有使用方释放之后再释放资源
- 实际的释放代码在同步块外面
-
通过canCacheNewValue()方法判断新值是否可以缓存,如果可以,则创建一个新的Entry对象并将其放入mCachedEntries中,并通过newClientReference()方法创建客户端引用。
-
调用maybeEvictEntries()方法,可能逐出不必要的缓存项,因为方法最开始更改了缓存参数,另外还有可能插入了新的缓存值。
-
返回客户端引用clientRef,供调用者使用。如果不为空,调用方应该使用返回的引用,如果为空,调用方使用原始引用。
@Override
public @Nullable CloseableReference<V> cache(
final K key,
final CloseableReference<V> valueRef,
final @Nullable EntryStateObserver<K> observer) {
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);
maybeEvictEntries();
return clientRef;
}
逐出
逐出操作会计算出需要清理的最大数量和大小,计算需要清理的旧条目。并且安全地释放资源。
-
maybeEvictEntries()是条目逐出的入口,前面有很多地方调用了它,流程如下
- 计算出需要清理的最大数量和大小,限制在规定的最大清理队列条目数和缓存大小范围内。
- 调用trimExclusivelyOwnedEntries()方法来获取需要清理的旧条目。
- 调用makeOrphans()方法来将这些旧条目标记为孤立状态,不再被其他条目引用。
- 释放同步锁后,调用maybeClose(),如果条目计数为0而且已经孤立,在这里将它释放掉。
public void maybeEvictEntries() {
ArrayList<Entry<K, V>> oldEntries;
synchronized (this) {
int maxCount =
Math.min(
mMemoryCacheParams.maxEvictionQueueEntries,
mMemoryCacheParams.maxCacheEntries - getInUseCount());
int maxSize =
Math.min(
mMemoryCacheParams.maxEvictionQueueSize,
mMemoryCacheParams.maxCacheSize - getInUseSizeInBytes());
oldEntries = trimExclusivelyOwnedEntries(maxCount, maxSize);
makeOrphans(oldEntries);
}
maybeClose(oldEntries);
}
-
trimExclusivelyOwnedEntries
- 判断当前mExclusiveEntries中的条目数量和大小是否小于等于传入的count和size,如果是,则不需要进行裁剪操作,直接返回null。
- 进入一个while循环,判断当前mExclusiveEntries中的条目数量和大小是否大于传入的count和size,如果是,则继续裁剪操作。
- 获取mExclusiveEntries中第一个条目的key,并从mExclusiveEntries和mCachedEntries中移除该条目,将其添加到oldEntries中。
- 循环直到mExclusiveEntries中的条目数量和大小都小于等于传入的count和size,然后返回oldEntries。
这里逐出mExclusiveEntries中第一个条目的key,存入mExclusiveEntries的操作在条目被释放的时候,所以第一个条目也就是最早不被使用的条目,也就是lru策略。
private synchronized ArrayList<Entry<K, V>> trimExclusivelyOwnedEntries(int count, int size) {
// fast path without array allocation if no eviction is necessary
if (mExclusiveEntries.getCount() <= count && mExclusiveEntries.getSizeInBytes() <= size) {
return null;
}
ArrayList<Entry<K, V>> oldEntries = new ArrayList<>();
while (mExclusiveEntries.getCount() > count || mExclusiveEntries.getSizeInBytes() > size) {
K key = mExclusiveEntries.getFirstKey()
mExclusiveEntries.remove(key);
oldEntries.add(mCachedEntries.remove(key));
}
return oldEntries;
}
总结
在本文中,我们深入探讨了Fresco框架中关于内存缓存的部分,主要围绕BitmapMemoryCacheProducer和LruCountingMemoryCache展开讨论。作为Fresco框架内存管理的核心组件,这两者共同构建了Fresco内存缓存的架构。
BitmapMemoryCacheProducer作为内存缓存的生产者,负责将位图数据存储到内存缓存中,并提供给下游消费者使用。它通过高效管理内存中的位图数据,实现了内存缓存的快速读取和存储,为Fresco框架的图片加载和展示提供了重要支持。
而LruCountingMemoryCache则是BitmapMemoryCacheProducer中的关键组件,实现了LRU算法用于管理缓存条目。通过对内部存储方式、对象引用、逐出操作等流程的详细讲解,我们深入了解了Fresco内存缓存的内部工作原理。特别是在逐出操作方面,通过裁剪旧条目来释放内存空间,保证了内存缓存的有效利用和系统的稳定性。
综合来看,Fresco框架通过BitmapMemoryCacheProducer和LruCountingMemoryCache这样的内存管理组件,构建了一个高效、稳定的内存缓存架构。合理利用内存缓存,不仅提高了应用程序的性能和用户体验,同时也减少了内存资源的浪费和系统负担。通过深入了解和学习Fresco内存缓存的架构和工作原理,我们可以更好地优化和管理应用程序的内存使用,为用户提供更流畅的图片加载和展示体验。
转载自:https://juejin.cn/post/7346021356679987263