Fresco缓存设计分析

作者 : 开心源码 本文共9640个字,预计阅读时间需要25分钟 发布时间: 2022-05-12 共169人阅读

(第一篇)Fresco架构设计赏析

本文是Fresco源码分析系列第二篇文章,主要来看一下Fresco中有关图片缓存的内容。

引言

先来回顾一下上一篇文章中的一幅图:

NetworkFetchSequence.png

这张图形容了Fresco在第一次网络图片时所经历的过程,从图中可以看出涉及到缓存的Producer共有4个:BitmapMemroyCacheGetProducerBitmapMemoryCacheProducerEncodedMemoryCacheProducerDiskCacheWriteProducerFresco在加载图片时会按照图中绿色箭头所示依次经过这四个缓存Producer,一旦在某个Producer得到图片请求结果,就会按照蓝色箭头所示把结果依次回调回来。简单详情一下这4个Producer的功能:

  1. BitmapMemroyCacheGetProducer: 这个Producer会去内存缓存中检查这次请求有没命中缓存,假如命中则将缓存的图片作为这次请求结果。
  2. BitmapMemoryCacheProducer: 这个Producer会监听其后面的ProducerResult,并把Result(CloseableImage)存入缓存。
  3. EncodedMemoryCacheProducer: 它也是一个内存缓存,不过它缓存的是未解码的图片,即图片原始字节。
  4. DiskCacheWriteProducer: 顾名思义,它负责把图片缓存到磁盘,它缓存的也是未解码的图片。获取图片时假如命中了磁盘缓存那么就返回缓存的结果。

本文主要探讨BitmapMemoryCacheProducerDiskCacheWriteProducer。在文章正式开始之前先理解一个概念:

解码图片与未解码图片

对于这两个概念可以这样简单的了解 :CloseableImage为解码的图片,而EncodeImage是未解码的图片。

CloseableImage

CloseableImage是一个接口,最常接触到的它的实现是CloseableStaticBitmap:

CloseableStaticBitmap.java

public class CloseableStaticBitmap extends CloseableBitmap {    private volatile Bitmap mBitmap;    ...}

就可以把CloseableStaticBitmap了解为Bitmap的封装。

EncodeImage

它内部其实是直接封装了图片的字节/图片的文件字节流:

EncodeImage.java

public class EncodedImage implements Closeable {    private final @Nullable CloseableReference<PooledByteBuffer> mPooledByteBufferRef;  //实际上未解码的图片的字节    private final @Nullable Supplier<FileInputStream> mInputStreamSupplier;  //直接缓存一个文件字节流, 我猜测用于渐进式jpeg图片加载等场景}

接下来继续分析:

Bitmap内存缓存 : BitmapMemoryCacheProducer

CacheProducer : 缓存的工作流程

BitmapMemroyCacheGetProducer派生自BitmapMemoryCacheProducer,与BitmapMemoryCacheProducer的不同就是只读不写而已。 大致看一下BitmapMemoryCacheProducer的缓存运作逻辑:

BitmapMemoryCacheProducer.java

public class BitmapMemoryCacheProducer implements Producer<CloseableReference<CloseableImage>> {    private final MemoryCache<CacheKey, CloseableImage> mMemoryCache; //图片缓存的实现    @Override    public void produceResults(Consumer<CloseableReference<CloseableImage>> consumer...){        //1.先去缓存中获取        CloseableReference<CloseableImage> cachedReference = mMemoryCache.get(cacheKey);        //2.命中缓存直接返回请求结果        if (cachedReference != null) {            consumer.onNewResult(cachedReference, BaseConsumer.simpleStatusForIsLast(isFinal));            return;        }        ...        //3.wrapConsumer来观察后续Producer的结果        Consumer<CloseableReference<CloseableImage>> wrappedConsumer = wrapConsumer(consumer..);        //4.让下一个Producer继续工作        mInputProducer.produceResults(wrappedConsumer, producerContext);    }    protected Consumer<CloseableReference<CloseableImage>> wrapConsumer(){        return new DelegatingConsumer<...>(consumer) {            @Override            public void onNewResultImpl(CloseableReference<CloseableImage> newResult...){                //5.缓存结果                newCachedResult = mMemoryCache.cache(cacheKey, newResult);                 //6.通知前面的Producer图片请求结果                getConsumer().onNewResult((newCachedResult != null) ? newCachedResult : newResult, status);            }        }    }}

它的主要流程图如下(后面两个缓存的流程与它基本相同,因而对于缓存整体流程只画这一次):

BitmapMemoryCacheProducer工作流.png

图中红色箭头和字体是正常网络加载图片(第一次)的步骤,这里我们来细看一下MemoryCache的实现:

内存缓存的实现 : MemoryCache

MemoryCache是一个接口,在这里它的对应实现是CountingMemoryCache, 先来看一下这个类的构造函数:

CountingMemoryCache.java

public class CountingMemoryCache<K, V> implements MemoryCache<K, V>, MemoryTrimmable {    //缓存的集合其实就是一个map,不过这个map使用 Lru 算法    final CountingLruMap<K, Entry<K, V>> mExclusiveEntries;      final CountingLruMap<K, Entry<K, V>> mCachedEntries;     public CountingMemoryCache(ValueDescriptor<V> valueDescriptor,CacheTrimStrategy cacheTrimStrategy,Supplier<MemoryCacheParams> memoryCacheParamsSupplier) {        mValueDescriptor = valueDescriptor;// 用来估算当前缓存实体的大小        mExclusiveEntries = new CountingLruMap<>(wrapValueDescriptor(valueDescriptor)); // 主要存放没有被引用的对象,它的所有元素肯定在 mCachedEntries 集合中存在        mCachedEntries = new CountingLruMap<>(wrapValueDescriptor(valueDescriptor));  // 主要缓存集合        mCacheTrimStrategy = cacheTrimStrategy; // trim缓存的策略 (其实就是指定了trim ratio)        mMemoryCacheParams = mMemoryCacheParamsSupplier.get();  //  通过 ImagePipelineConfig 来配置的缓存参数    }    ...}

通过构造函数可以知道CountingMemoryCache一共含有两个缓存集合 :

  • mCachedEntries : 它是用来存放所有缓存对象的集合
  • mExclusiveEntries: 它是用来存放当前没有被引用的对象,在trim缓存是,主要是trim掉这个缓存集合的中的对象。

CountingMemoryCache的缓存逻辑主要是围绕这两个集合开展的。接下来看一下它的cacheget的方法(这两个方法是缓存的核心方法)。

将图片保存到内存缓存 : CountingMemoryCache.cache()

public  CloseableReference<V> cache(K key, CloseableReference<V> valueRef, EntryStateObserver<K> observer) {    Entry<K, V> oldExclusive;    CloseableReference<V> oldRefToClose = null;    CloseableReference<V> clientRef = null;    synchronized (this) {        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); // 构造一个缓存实体(Entry)            mCachedEntries.put(key, newEntry);   //缓存            clientRef = newClientReference(newEntry);        }    }    CloseableReference.closeSafely(oldRefToClose);    //可能会调用到 release 方法,    ...    return clientRef;}

上面代码我做了比较详细的注释。简单的讲就是把这个对象放入到mCachedEntries集合中,假如原来就已经缓存了这个对象,那么就要把它先从mCachedEntriesmExclusiveEntries集合中移除。

Fresco的默认内存缓存大小

上面canCacheNewValue()是用来判断当前缓存能否已经达到了最大值。那Fresco内存缓存的最大值是多少呢?这个值可以通过ImagePipelineConfig来配置,假如没有配置的话默认配置是:DefaultBitmapMemoryCacheParamsSupplier:

DefaultBitmapMemoryCacheParamsSupplier.java

public class DefaultBitmapMemoryCacheParamsSupplier implements Supplier<MemoryCacheParams> {    ...    private int getMaxCacheSize() {        final int maxMemory = Math.min(mActivityManager.getMemoryClass() * ByteConstants.MB, Integer.MAX_VALUE);        if (maxMemory < 32 * ByteConstants.MB) {            return 4 * ByteConstants.MB;        } else if (maxMemory < 64 * ByteConstants.MB) {            return 6 * ByteConstants.MB;        } else {            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {                return 8 * ByteConstants.MB;            } else {                return maxMemory / 4;            }        }    }}

Fresco的默认缓存大小是根据当前应用的运行内存来决定的,对于应用运行内存达到64MB以上的手机(现在的手机普遍已经大于这个值了),Fresco的默认缓存大小是maxMemory / 4

从内存缓存中获取图片 : CountingMemoryCache.get()

缓存获取的逻辑也很简单:

CountingMemoryCache.java

    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);            }        }        maybeNotifyExclusiveEntryRemoval(oldExclusive);        maybeUpdateCacheParams();        maybeEvictEntries();        return clientRef;    }

即从mCachedEntries集合中获取,假如mExclusiveEntries集合中存在的话就移除。

trim策略 : CountingMemoryCache.getrimt()

当内存缓存达到峰值或者系统内存不足时就需要对当前的内存缓存做trim操作, trim时是基于Lru算法的,我们看一下它的具体逻辑:

  public void trim(MemoryTrimType trimType) {    ArrayList<Entry<K, V>> oldEntries;    //根据当前的应用状态来确定trim ratio。 应用状态是指: 应用处于前端、后端等等    final double trimRatio = mCacheTrimStrategy.getTrimRatio(trimType);    ...    int targetCacheSize = (int) (mCachedEntries.getSizeInBytes() * (1 - trimRatio));  // trim到当前缓存的多少    int targetEvictionQueueSize = Math.max(0, targetCacheSize - getInUseSizeInBytes());  // 究竟能trim多大    oldEntries = trimExclusivelyOwnedEntries(Integer.MAX_VALUE, targetEvictionQueueSize); //trim  mExclusiveEntries集合 集合中的对象    makeOrphans(oldEntries);    ...  }

trim操作的主要步骤是:

  1. 根据当前应用的状态决定trim ratio (应用状态是指应用处于前端、后端等等)。
  2. 根据trim ratio来算出经过trim后缓存的大小targetCacheSize
  3. 根据mExclusiveEntries集合的大小来决定究竟能trim多少 (能trim的最大就是mExclusiveEntries.size)
  4. mExclusiveEntries集合做trim操作,即移除其中的元素。

trim时最大能trim掉的大小是mExclusiveEntries集合的大小。所以假如当前应用存在内存泄漏,导致mExclusiveEntries中的元素很少,那么trim操作几乎是没有效果的。

Bitmap编码内存缓存 : EncodedMemoryCacheProducer

这个缓存Producer的工作逻辑和BitmapMemoryCacheProducer相同,不同的是它缓存的对象:

public class EncodedMemoryCacheProducer implements Producer<EncodedImage> {    private final MemoryCache<CacheKey, PooledByteBuffer> mMemoryCache;     ...}

即它缓存的是PooledByteBuffer, 它是什么东西呢? 它牵扯到Fresco编码图片的内存管理,这些内容我会单开一篇文章来讲一下。这里就先不说了。PooledByteBuffer你可以简单的把它当成一个字节数组。

Bitmap磁盘缓存 : DiskCacheWriteProducer

它是Fresco图片磁盘缓存的逻辑管理者,整个缓存逻辑和BitmapMemoryCacheProducer差不多:

public class DiskCacheWriteProducer implements Producer<EncodedImage> {    private final BufferedDiskCache mDefaultBufferedDiskCache; /    ...    private static class DiskCacheWriteConsumer extends DelegatingConsumer<EncodedImage, EncodedImage> {        @Override        public void onNewResultImpl(EncodedImage newResult, @Status int status) {            ...            mDefaultBufferedDiskCache.put(cacheKey, newResult);        }    }}

接下来我们主要看一下它的磁盘存储逻辑(怎样存), 对于存储逻辑是由BufferedDiskCache来负责的:

BufferedDiskCache

先来看一下类的组成结构:

public class BufferedDiskCache {    private final FileCache mFileCache;  // 文件存储的实现    private final Executor mWriteExecutor; //存储文件时的线程    private final StagingArea mStagingArea; }
  • FileCache : 将EncodeImage保存到磁盘存储实现。
  • Executor : 指定文件保存操作所运行的线程。
  • StagingArea: 相似于git中的stage概念,它是一个map,用于保存当前正在进行磁盘缓存操作。

将图片保存至磁盘 : BufferedDiskCache.put()

这个方法主要负责往磁盘缓存一张图片:

public void put(final CacheKey key, EncodedImage encodedImage) {    ..    mStagingArea.put(key, encodedImage); //把这次缓存操作放到暂存区    ...    final EncodedImage finalEncodedImage = EncodedImage.cloneOrNull(encodedImage);    mWriteExecutor.execute( //开启写入线程        new Runnable() {            @Override            public void run() {            try {                writeToDiskCache(key, finalEncodedImage); //写入到磁盘            } finally {                mStagingArea.remove(key, finalEncodedImage); //从操作暂存区中移除这次操作                EncodedImage.closeSafely(finalEncodedImage);            }            }        });    }    ...}

writeToDiskCache()主要调用mFileCache.insert()来把图片保存到磁盘:

mFileCache.insert(key, new WriterCallback() {        @Override        public void write(OutputStream os) throws IOException {            mPooledByteStreams.copy(encodedImage.getInputStream(), os); //实际上就是把encodeImage 写入到 os(OutputStream) 中        }    });

至于mFileCache.insert()的具体实现涉及的源码较多,考虑文章篇幅的起因这里我不去具体跟了。简单的总结一下其实现步骤和少量关键点:

Step1 : 生成ResourceId

这个ResourceId可以简单的了解为缓存文件的文件名,它的生成算法如下:

SecureHashUtil.makeSHA1HashBase64(key.getUriString().getBytes("UTF-8"));  // key就是CacheKey

SHA-1 + Base64

Step2 : 创立临时文件,并把图片写入到临时文件中

创立临时文件

public File createTempFile(File parent) throws IOException {    return File.createTempFile(resourceId + ".", TEMP_FILE_EXTENSION, parent);}

把图片写入到这个临时文件中 : DefaultDiskStorage.java

 public void writeData(WriterCallback callback, Object debugInfo) throws IOException {    FileOutputStream fileStream = new FileOutputStream(mTemporaryFile);    ...    CountingOutputStream countingStream = new CountingOutputStream(fileStream);    callback.write(countingStream);    countingStream.flush();

这里的callback(WriterCallback)就是mFileCache.insert()方法传入的那个callback -> { mPooledByteStreams.copy(encodedImage.getInputStream(), os); }

Step3 : 把临时文件重命名为resourceId
Step4 : 设置好文件的最后修改时间

从磁盘中获取文件 : BufferedDiskCache.get()

读就是写的逆操作,这里不做具体分析了。

OK,到这里本文就算结束了。下一篇文章会继续讨论Fresco的EncodeImage的内存管理,欢迎继续关注。

欢迎关注我的Android进阶计划看更多干货

欢迎关注我的微信公众号:susion随心

微信公众号.jpeg

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Fresco缓存设计分析

发表回复