结合 Fresco 源码详细讲解其缓存机制

226 阅读6分钟

以下将结合 Fresco 源码详细讲解其缓存机制,包括内存缓存、编码缓存和磁盘缓存的设计与实现,分析其工作原理、优缺点。Fresco 的缓存机制是其高效加载图片和优化内存管理的核心,采用多级缓存架构以最大化性能和用户体验。


Fresco 缓存机制概述

Fresco 的缓存机制分为三层,分别针对不同阶段的图片数据:

  1. Bitmap 缓存(内存缓存):存储解码后的 Bitmap 或 CloseableImage,用于快速渲染到 UI。
  2. 编码缓存(Encoded Cache,内存缓存):存储未解码的原始图片数据(字节流),避免重复下载或解码。
  3. 磁盘缓存(Disk Cache):持久化存储图片数据,支持小图和大图分开存储,减少网络请求。

这些缓存通过 CacheKey 唯一标识资源,ImagePipeline 按优先级(Bitmap 缓存 → 编码缓存 → 磁盘缓存 → 网络/本地)查询数据。以下是详细讲解。


  1. Bitmap 缓存(内存缓存)

实现原理

  • 存储对象:解码后的 Bitmap 或 CloseableImage(如 CloseableBitmap 或 CloseableStaticBitmap)。

  • 实现类:CountingMemoryCache,基于 LRU(最近最少使用)算法,管理内存中的 Bitmap。

  • 管理机制:

    • 使用 CloseableReference 包装 Bitmap,通过引用计数确保内存安全。
    • 缓存大小通过 MemoryCacheParams 配置,限制最大条目数和内存占用。
    • 支持缓存淘汰(eviction)和修剪(trim)以应对内存压力。
  • 源码分析:

    • CountingMemoryCache 的核心方法:

      java

      public class CountingMemoryCache<K, V> implements MemoryCache<K, V> {
          private final LruCache<K, ValueDescriptor<V>> mCachedEntries = new LruCache<>();
          
          public CloseableReference<V> cache(K key, CloseableReference<V> value) {
              ValueDescriptor<V> oldValue = mCachedEntries.put(key, value);
              if (oldValue != null) {
                  CloseableReference.closeSafely(oldValue);
              }
              return CloseableReference.cloneOrNull(value);
          }
          
          public CloseableReference<V> get(K key) {
              ValueDescriptor<V> value = mCachedEntries.get(key);
              return value != null ? CloseableReference.cloneOrNull(value.get()) : null;
          }
      }
      
    • cache 方法存储 Bitmap,get 方法查询缓存,CloseableReference 确保资源正确释放。

    • 内存压力下,trimToNothing 或 evictAll 清空缓存:

      java

      public void trim(MemoryTrimType trimType) {
          if (trimType == MemoryTrimType.OnCloseToDalvikHeapLimit) {
              evictOldest(maxEvictionSize);
          } else if (trimType == MemoryTrimType.OnSystemLowMemory) {
              trimToNothing();
          }
      }
      

查询流程

  • ImagePipeline 通过 CacheKey 查询 BitmapMemoryCache:

    java

    CloseableReference<CloseableImage> cachedImage = mBitmapMemoryCache.get(cacheKey);
    if (cachedImage != null) {
        return DataSourceSupplier.createSuccessfulDataSource(cachedImage);
    }
    
  • 如果命中,直接返回 CloseableImage,用于渲染。


  1. 编码缓存(Encoded Cache,内存缓存)

实现原理

  • 存储对象:未解码的图片数据(PooledByteBuffer),通常是 JPEG 或 PNG 的字节流。

  • 实现类:BufferedDiskCache(尽管名字中有 Disk,编码缓存通常在内存中)或 MemoryCache。

  • 管理机制:

    • 使用 PooledByteBuffer 管理字节流,基于内存池复用缓冲区,减少内存分配。
    • 缓存大小通过 MemoryCacheParams 配置,LRU 策略淘汰。
    • 编码缓存命中后,数据需解码为 Bitmap。
  • 源码分析:

    • EncodedMemoryCache 的存储逻辑:

      java

      public CloseableReference<PooledByteBuffer> cache(CacheKey key, CloseableReference<PooledByteBuffer> value) {
          return mMemoryCache.cache(key, value);
      }
      
    • 查询逻辑:

      java

      CloseableReference<PooledByteBuffer> encodedImage = mEncodedMemoryCache.get(cacheKey);
      if (encodedImage != null) {
          return decodeEncodedImage(encodedImage);
      }
      

查询流程

  • 如果 Bitmap 缓存未命中,ImagePipeline 查询编码缓存:

    java

    CloseableReference<PooledByteBuffer> encodedImage = mEncodedMemoryCache.get(cacheKey);
    if (encodedImage != null) {
        return mImageDecoder.decode(encodedImage, imageRequest.getImageDecodeOptions());
    }
    
  • 命中后,调用 ImageDecoder 解码为 CloseableImage。


  1. 磁盘缓存(Disk Cache)

实现原理

  • 存储对象:未解码的图片数据,存储在设备文件系统中。

  • 实现类:DiskStorageCache,基于 DiskStorage(如 DynamicDefaultDiskStorage)。

  • 管理机制:

    • 支持小图(small)和大图(default)分开存储,优化 IO 性能。
    • 缓存大小通过 DiskCacheConfig 配置,限制最大文件数和存储空间。
    • 使用 LRU 淘汰策略,定期清理过期或不常用的文件。
    • 磁盘缓存支持版本控制,防止旧版本数据冲突。
  • 源码分析:

    • DiskStorageCache 的存储逻辑:

      java

      public class DiskStorageCache implements FileCache {
          private final DiskStorage mStorage;
          
          public void insert(CacheKey key, WriterCallback callback) throws IOException {
              String resourceId = key.getUriString();
              mStorage.insert(resourceId, callback);
          }
          
          public BinaryResource getResource(CacheKey key) {
              String resourceId = key.getUriString();
              return mStorage.getResource(resourceId, key);
          }
      }
      
    • 缓存清理:

      java

      public void clearOldEntries(long cacheExpirationMs) {
          mStorage.purgeUnexpectedResources();
          mStorage.clearOldEntries(cacheExpirationMs);
      }
      

查询流程

  • 如果内存缓存未命中,查询磁盘缓存:

    java

    BinaryResource resource = mMainDiskCache.getResource(cacheKey);
    if (resource != null) {
        PooledByteBuffer buffer = readResourceToBuffer(resource);
        return decodeEncodedImage(CloseableReference.of(buffer));
    }
    
  • 命中后,数据读入 PooledByteBuffer,解码为 CloseableImage。


缓存查询完整流程

ImagePipeline 的缓存查询遵循以下顺序:

  1. Bitmap 缓存:查询 CountingMemoryCache,命中直接返回 CloseableImage。
  2. 编码缓存:查询 EncodedMemoryCache,命中后解码为 CloseableImage。
  3. 磁盘缓存:查询 DiskStorageCache,命中后读取数据,存入编码缓存并解码。
  4. 网络/本地:缓存未命中,触发 NetworkFetcher 或其他 Producer 下载/读取数据,存入各级缓存。

源码示例(ImagePipeline):

java

public DataSource<CloseableReference<CloseableImage>> fetchDecodedImage(ImageRequest request) {
    CacheKey cacheKey = mCacheKeyFactory.getBitmapCacheKey(request);
    // 1. 查询 Bitmap 缓存
    CloseableReference<CloseableImage> cachedImage = mBitmapMemoryCache.get(cacheKey);
    if (cachedImage != null) {
        return ImmediateDataSource.create(cachedImage);
    }
    // 2. 查询编码缓存
    CloseableReference<PooledByteBuffer> encodedImage = mEncodedMemoryCache.get(cacheKey);
    if (encodedImage != null) {
        return decodeFromEncodedImage(encodedImage, request);
    }
    // 3. 查询磁盘缓存
    return fetchFromDiskCacheOrNetwork(request, cacheKey);
}

缓存机制的优缺点

优点

  1. 高效性:

    • 多级缓存减少重复下载和解码,Bitmap 缓存直接提供渲染数据,编码缓存避免重复解码,磁盘缓存减少网络请求。
    • LRU 策略确保高频图片驻留缓存,提升命中率。
  2. 内存优化:

    • CloseableReference 和 PooledByteBuffer 严格管理内存,防止泄漏。
    • 内存池复用缓冲区,减少 GC 压力。
  3. 灵活性:

    • DiskCacheConfig 支持小图和大图分开存储,优化 IO。
    • CacheKeyFactory 允许自定义缓存键,支持复杂场景(如不同分辨率图片)。
  4. 渐进式加载支持:

    • 编码缓存和磁盘缓存支持渐进式 JPEG,部分数据即可解码显示。
  5. 可扩展性:

    • 开发者可自定义 DiskStorage 或 CacheKeyFactory,适配特定需求。

缺点

  1. 复杂性:

    • 多级缓存和 Producer 链增加了代码复杂度和维护成本,开发者需理解 ImagePipeline 的工作原理。
    • 源码中的异步回调和线程切换可能导致调试困难。
  2. 内存占用:

    • Bitmap 缓存存储解码后的图片,在低内存设备上可能触发频繁的缓存淘汰或 OOM。
    • 编码缓存和内存池也占用额外内存,需仔细配置 MemoryCacheParams。
  3. 磁盘 IO 开销:

    • 磁盘缓存的读写操作可能成为瓶颈,尤其在高并发场景或低性能设备上。
    • 小图和大图分开存储增加了管理复杂度。
  4. 缓存一致性:

    • 缓存更新机制依赖 CacheKey,如果图片内容变化但 URI 未变,可能导致显示旧数据。
    • 开发者需手动清理缓存或实现版本控制。
  5. 学习曲线:

    • 相较于 Glide 或 Picasso,Fresco 的缓存配置和自定义选项更复杂,初学者可能难以快速上手。

优化建议(基于源码)

  1. 调整缓存大小:

    • 通过 ImagePipelineConfig 配置 MemoryCacheParams 和 DiskCacheConfig,适配设备内存和存储:

      java

      ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
          .setBitmapMemoryCacheParamsSupplier(() -> new MemoryCacheParams(
              maxCacheSize, maxCacheEntries, maxEvictionSize, maxEvictionEntries, maxCacheEntrySize))
          .setMainDiskCacheConfig(DiskCacheConfig.newBuilder(context)
              .setMaxCacheSize(maxDiskCacheSize)
              .build())
          .build();
      
  2. 自定义 CacheKey:

    • 实现 CacheKeyFactory 区分不同分辨率或后处理的图片:

      java

      public class CustomCacheKeyFactory implements CacheKeyFactory {
          public CacheKey getBitmapCacheKey(ImageRequest request, Object callerContext) {
              return new SimpleCacheKey(request.getSourceUri() + "_" + request.getResizeOptions());
          }
      }
      
  3. 优化磁盘缓存:

    • 启用小图和大图分开存储,减少 IO:

      java

      DiskCacheConfig smallCache = DiskCacheConfig.newBuilder(context)
          .setBaseDirectoryName("small_images")
          .setMaxCacheSize(10 * 1024 * 1024) // 10MB
          .build();
      
  4. 监控缓存命中率:

    • 使用 ImagePipeline 的 CacheEventListener 监控缓存性能,调整策略:

      java

      public class CustomCacheEventListener implements CacheEventListener {
          public void onHit(CacheEvent cacheEvent) {
              Log.d("Cache", "Cache hit: " + cacheEvent.getCacheKey());
          }
      }
      

总结

Fresco 的缓存机制通过 Bitmap 缓存、编码缓存和磁盘缓存三层架构,结合 CountingMemoryCache、BufferedDiskCache 和 DiskStorageCache 的实现,实现了高效的图片加载和内存管理。其优点在于高性能、内存安全和灵活性,但缺点包括复杂性、内存占用和磁盘 IO 开销。开发者可通过配置 ImagePipelineConfig 和自定义 CacheKey 优化缓存行为。