Fresco框架(二)-- 内存缓存

在上一篇追踪源码,梳理了一下Fresco加载中涉及到的一些对象,以及在这个架构中如何完成请求。然后分析了网络请求的加载过程,分析了ProducerSequence的组成部分,一期完整的请求流程需要经过如下对象和工作:

  1. BitmapMemoryCacheGetProducer,图片缓存读取

  2. ThreadHandoffProducer,线程切换

  3. BitmapMemoryCacheKeyMultiplexProducer,多路复用,相同请求进行合并

  4. BitmapMemoryCacheProducer,图片缓存

  5. ResizeAndRotateProducer,图片调整

  6. AddImageTransformMetaDataProducer,元数据解码

  7. EncodedCacheKeyMultiplexProducer,元数据多路复用

  8. EncodedMemoryCacheProducer,元数据缓存

  9. DiskCacheReadProducer,磁盘缓存读取

  10. DiskCacheWriteProducer,磁盘缓存写入

  11. WebpTranscodeProducer,webp转码

  12. NetworkFetchProducer,网络请求

其中总共具有三层缓存

  1. 第一层Bitmap缓存,Bitmap缓存存储Bitmap对象,这些Bitmap对象可以立即用来显示,在线程切换之前就读缓存,缓存在内存当中,在后台会被清掉,Bitmap相对于元数据会大很多,参考之前的Bitmap相关知识

  2. 第二层元数据缓存,元数据缓存存储原始压缩格式图片如png、jpg,这些缓存在使用时需要先解码成bitmap,使用会再次缓存到第一层缓存,缓存在内存中,在后台会被清掉

  3. 第三层元数据缓存,与第二层的缓存数据完全一致,使用时需要解码,使用会再次缓存到第一层和第二层缓存中,缓存在磁盘中,在后台不回被清除

BitmapMemoryCacheProducer

内存缓存是一种用于存储图片数据的临时存储空间,可以快速地访问和加载图片资源,提高图片加载的效率和性能。内存缓存通常存储在RAM中,因此可以快速地读取和写入数据。

介绍

Fresco的缓存架构中,前两层都是使用的内存缓存,分别针对的是Bitmap数据和元数据:

  • Bitmap缓存涉及两个Producer,BitmapMemoryCacheGetProducer和BitmapMemoryCacheProducer。

    • BitmapMemoryCacheGetProducer继承自BitmapMemoryCacheProducer,禁止了其写缓存的能力。所以逻辑还是在BitmapMemoryCacheProducer中。
    • 当图片从网络或本地加载后,经过解码生成位图后,BitmapMemoryCacheProducer会将这些位图数据存储到内存缓存中。下次再次加载相同的图片时,可以直接从内存缓存中读取位图数据,避免重新解码,提高图片加载的速度和效率。
  • 元数据缓存EncodedMemoryCacheProducer,负责将原始的编码数据存储到编码内存缓存中。当图片从网络或本地加载后,未经过解码的编码数据会被EncodedMemoryCacheProducer存储到编码内存缓存中。这样在需要重新加载图片时,可以直接从编码内存缓存中读取原始的编码数据,再解码生成位图,避免重新下载图片,提高加载速度。

从功能上可以看出,BitmapMemoryCacheProducer和EncodedMemoryCacheProducer除了针对的对象不同之外,逻辑是完全相同的。

代码流程

  1. 当BitmapMemoryCacheProducer的produceResults方法被调用时,首先从ProducerContext中获取到对应的CacheKey。
  2. 接着通过CacheKey从内存缓存中查找是否有对应的位图数据。如果内存缓存中有对应的位图数据,则直接将数据返回给Consumer,并关闭CloseableReference。
  3. 如果内存缓存中没有对应的位图数据,则调用下一个生产者(mInputProducer)的produceResults方法,同时将一个包装过的Consumer传入其中。
  4. 包装过的Consumer当从下一个生产者获取到新的结果时,将结果存储到内存缓存中,并继续传递结果给原始的Consumer。

其中缓存获取和存储分别是通过MemoryCache的get和put方法实现。

swift 复制代码
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算法会根据最近访问的顺序来淘汰最少使用的数据,以保持缓存大小在一定范围内。

存储方式和对象

  1. 存储方式

    1. mExclusiveEntries存储的是那些不被任何客户端使用的缓存条目,这些是可被清理的、空闲的缓存条目,因此这些条目可以被驱逐出缓存,当一个条目不再被使用时,会被移动到mExclusiveEntries
    2. mCachedEntries则是存储所有缓存条目的地方,包括那些被标记为独占的条目和普通的缓存条目。
swift 复制代码
final CountingLruMap<K, Entry<K, V>> mExclusiveEntries;
final CountingLruMap<K, Entry<K, V>> mCachedEntries;
  1. 存储对象

    1. key是条目的键,用于唯一标识该条目在缓存中的位置。
    2. valueRef是一个CloseableReference类型的成员变量,用于存储条目对应的值的引用。
    3. clientCount表示引用该条目值的客户端数量,即有多少个客户端正在使用这个值。
    4. isOrphan表示该条目是否孤立,孤立的条目意味着这个条目不再被缓存管理器追踪。
arduino 复制代码
class Entry<K, V> {
  public final K key;
  public final CloseableReference<V> valueRef;
  public int clientCount;
  public boolean isOrphan;
}

读取

从缓存中获取指定key对应的值,如果有缓存返回一个引用,如果没有缓存返回空。

  1. 从mExclusiveEntries中移除对应的Entry。
  2. 从mCachedEntries中取对应的Entry。
  3. 如果获取到了Entry,则调用newClientReference()创建一个新的CloseableReference引用。
  4. maybeUpdateCacheParams()和maybeEvictEntries(),第一个可能会根据缓存的大小、存储策略等因素来更新缓存的相关参数。第二个可能逐出不必要的缓存项,因为上一步可能更改了缓存参数,导致需要重新计算。
ini 复制代码
@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引用,供给外面的调用方单独使用,使用完成后对引用进行释放,通过这样实现了一对多的管理方式。

  1. newClientReference方法用于创建一个新的引用

    1. 首先通过increaseClientCount()增加计数,表示有一个新的引用该条目值。
    2. 然后创建一个CloseableReference,管理对条目值的引用,并在引用不再需要时释放资源。
    3. 当引用需要释放时,会调用releaseClientReference()方法释放引用。
java 复制代码
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);
        }
      });
}
  1. releaseClientReference方法用于释放引用,

    1. 如减少客户端计数、可能将条目添加到独占集合、关闭旧的引用等。
    2. 首先,减少了条目的客户端计数,表示有一个客户端不再引用该条目值。
    3. maybeAddToExclusives(),如果计数降为0但是没有孤立,表示条目不再被使用时,会被移动到mExclusiveEntries。
    4. referenceToClose(),计数降为0而且已经孤立,发生在缓存已经因为其他情况清除掉的情况下,并且当前释放的已经是条目最后一个引用,在这里将它释放掉。
    5. maybeUpdateCacheParams()和maybeEvictEntries(),第一个可能会根据缓存的大小、存储策略等因素来更新缓存的相关参数。第二个可能逐出不必要的缓存项,因为上一步可能更改了缓存参数,导致需要重新计算。
scss 复制代码
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,供调用者使用。如果不为空,调用方应该使用返回的引用,如果为空,调用方使用原始引用。

ini 复制代码
@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;
}

逐出

逐出操作会计算出需要清理的最大数量和大小,计算需要清理的旧条目。并且安全地释放资源。

  1. maybeEvictEntries()是条目逐出的入口,前面有很多地方调用了它,流程如下

    1. 计算出需要清理的最大数量和大小,限制在规定的最大清理队列条目数和缓存大小范围内。
    2. 调用trimExclusivelyOwnedEntries()方法来获取需要清理的旧条目。
    3. 调用makeOrphans()方法来将这些旧条目标记为孤立状态,不再被其他条目引用。
    4. 释放同步锁后,调用maybeClose(),如果条目计数为0而且已经孤立,在这里将它释放掉。
ini 复制代码
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);
}
  1. trimExclusivelyOwnedEntries

    1. 判断当前mExclusiveEntries中的条目数量和大小是否小于等于传入的count和size,如果是,则不需要进行裁剪操作,直接返回null。
    2. 进入一个while循环,判断当前mExclusiveEntries中的条目数量和大小是否大于传入的count和size,如果是,则继续裁剪操作。
    3. 获取mExclusiveEntries中第一个条目的key,并从mExclusiveEntries和mCachedEntries中移除该条目,将其添加到oldEntries中。
    4. 循环直到mExclusiveEntries中的条目数量和大小都小于等于传入的count和size,然后返回oldEntries。

这里逐出mExclusiveEntries中第一个条目的key,存入mExclusiveEntries的操作在条目被释放的时候,所以第一个条目也就是最早不被使用的条目,也就是lru策略。

arduino 复制代码
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内存缓存的架构和工作原理,我们可以更好地优化和管理应用程序的内存使用,为用户提供更流畅的图片加载和展示体验。

相关推荐
Java探秘者5 分钟前
Maven下载、安装与环境配置详解:从零开始搭建高效Java开发环境
java·开发语言·数据库·spring boot·spring cloud·maven·idea
攸攸太上5 分钟前
Spring Gateway学习
java·后端·学习·spring·微服务·gateway
2301_7869643611 分钟前
3、练习常用的HBase Shell命令+HBase 常用的Java API 及应用实例
java·大数据·数据库·分布式·hbase
2303_8120444614 分钟前
Bean,看到P188没看了与maven
java·开发语言
苹果醋314 分钟前
大模型实战--FastChat一行代码实现部署和各个组件详解
java·运维·spring boot·mysql·nginx
秋夫人16 分钟前
idea 同一个项目不同模块如何设置不同的jdk版本
java·开发语言·intellij-idea
m0_6640470221 分钟前
数字化采购管理革新:全过程数字化采购管理平台的架构与实施
java·招投标系统源码
aqua353574235841 分钟前
蓝桥杯-财务管理
java·c语言·数据结构·算法
Deryck_德瑞克41 分钟前
Java网络通信—TCP
java·网络·tcp/ip
砥砺code42 分钟前
【2024版本】Mac/Windows IDEA安装教程
java