
(第一篇)Fresco架构设计赏析
本文是
Fresco源码分析系列第二篇文章,主要来看一下Fresco中有关图片缓存的内容。
先来回顾一下上一篇文章中的一幅图:
NetworkFetchSequence.png这张图形容了Fresco在第一次网络图片时所经历的过程,从图中可以看出涉及到缓存的Producer共有4个:BitmapMemroyCacheGetProducer、BitmapMemoryCacheProducer、EncodedMemoryCacheProducer和DiskCacheWriteProducer。Fresco在加载图片时会按照图中绿色箭头所示依次经过这四个缓存Producer,一旦在某个Producer得到图片请求结果,就会按照蓝色箭头所示把结果依次回调回来。简单详情一下这4个Producer的功能:
BitmapMemroyCacheGetProducer: 这个Producer会去内存缓存中检查这次请求有没命中缓存,假如命中则将缓存的图片作为这次请求结果。BitmapMemoryCacheProducer: 这个Producer会监听其后面的Producer的Result,并把Result(CloseableImage)存入缓存。EncodedMemoryCacheProducer: 它也是一个内存缓存,不过它缓存的是未解码的图片,即图片原始字节。DiskCacheWriteProducer: 顾名思义,它负责把图片缓存到磁盘,它缓存的也是未解码的图片。获取图片时假如命中了磁盘缓存那么就返回缓存的结果。本文主要探讨BitmapMemoryCacheProducer和DiskCacheWriteProducer。在文章正式开始之前先理解一个概念:
对于这两个概念可以这样简单的了解 :CloseableImage为解码的图片,而EncodeImage是未解码的图片。
CloseableImage是一个接口,最常接触到的它的实现是CloseableStaticBitmap:
CloseableStaticBitmap.java
public class CloseableStaticBitmap extends CloseableBitmap { private volatile Bitmap mBitmap; ...}就可以把CloseableStaticBitmap了解为Bitmap的封装。
它内部其实是直接封装了图片的字节/图片的文件字节流:
EncodeImage.java
public class EncodedImage implements Closeable { private final @Nullable CloseableReference<PooledByteBuffer> mPooledByteBufferRef; //实际上未解码的图片的字节 private final @Nullable Supplier<FileInputStream> mInputStreamSupplier; //直接缓存一个文件字节流, 我猜测用于渐进式jpeg图片加载等场景}接下来继续分析:
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是一个接口,在这里它的对应实现是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的缓存逻辑主要是围绕这两个集合开展的。接下来看一下它的cache和get的方法(这两个方法是缓存的核心方法)。
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集合中,假如原来就已经缓存了这个对象,那么就要把它先从mCachedEntries和mExclusiveEntries集合中移除。
上面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.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操作, 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操作的主要步骤是:
trim ratio (应用状态是指应用处于前端、后端等等)。trim ratio来算出经过trim后缓存的大小targetCacheSizemExclusiveEntries集合的大小来决定究竟能trim多少 (能trim的最大就是mExclusiveEntries.size)mExclusiveEntries集合做trim操作,即移除其中的元素。即trim时最大能trim掉的大小是mExclusiveEntries集合的大小。所以假如当前应用存在内存泄漏,导致mExclusiveEntries中的元素很少,那么trim操作几乎是没有效果的。
这个缓存Producer的工作逻辑和BitmapMemoryCacheProducer相同,不同的是它缓存的对象:
public class EncodedMemoryCacheProducer implements Producer<EncodedImage> { private final MemoryCache<CacheKey, PooledByteBuffer> mMemoryCache; ...}即它缓存的是PooledByteBuffer, 它是什么东西呢? 它牵扯到Fresco编码图片的内存管理,这些内容我会单开一篇文章来讲一下。这里就先不说了。PooledByteBuffer你可以简单的把它当成一个字节数组。
它是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来负责的:
先来看一下类的组成结构:
public class BufferedDiskCache { private final FileCache mFileCache; // 文件存储的实现 private final Executor mWriteExecutor; //存储文件时的线程 private final StagingArea mStagingArea; }EncodeImage保存到磁盘存储实现。这个方法主要负责往磁盘缓存一张图片:
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()的具体实现涉及的源码较多,考虑文章篇幅的起因这里我不去具体跟了。简单的总结一下其实现步骤和少量关键点:
这个ResourceId可以简单的了解为缓存文件的文件名,它的生成算法如下:
SecureHashUtil.makeSHA1HashBase64(key.getUriString().getBytes("UTF-8")); // key就是CacheKey即SHA-1 + Base64。
创立临时文件
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); }
读就是写的逆操作,这里不做具体分析了。
OK,到这里本文就算结束了。下一篇文章会继续讨论
Fresco的EncodeImage的内存管理,欢迎继续关注。
欢迎关注我的Android进阶计划看更多干货
欢迎关注我的微信公众号:susion随心
微信公众号.jpeg