Flutter开发——图片加载与缓存源码解析

在Flutter中有个图片组件:Image,通常会使用它的Image.network(src)Image.file(src)Image.asset(src)来加载图片。
下面是Image的普通构造方法:

  const Image({
    super.key,
    required this.image,
    this.frameBuilder,
    this.loadingBuilder,
    this.errorBuilder,
    this.semanticLabel,
    this.excludeFromSemantics = false,
    this.width,
    this.height,
    this.color,
    this.opacity,
    this.colorBlendMode,
    this.fit,
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.centerSlice,
    this.matchTextDirection = false,
    this.gaplessPlayback = false,
    this.isAntiAlias = false,
    this.filterQuality = FilterQuality.low,
  })

从它的构造方法可以看出Image组件有个必传参数image,它是ImageProvider类型。ImageProvider是个抽象类,定义了图片数据获取和加载的相关接口。它的主要职责有两个:

  • 1.提供图片数据源;
  • 2.缓存图片;

ImageProvider抽象类:

abstract class ImageProvider<T extends Object> {
  const ImageProvider();
  
  ImageStream resolve(ImageConfiguration configuration) {
    ...
  }
  
  ImageStream createStream(ImageConfiguration configuration) {
    return ImageStream();
  }

  
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
    ...
  }
  
  Future<bool> evict({ ImageCache? cache, ImageConfiguration configuration = ImageConfiguration.empty }) async {
    ...
  }
  
  Future<T> obtainKey(ImageConfiguration configuration);
}

从上面源码中可以发现图片的加载和解析是由ImageProvider来完成的,具体说是由它的子类实现的。ImageProvider派生了很多子类,例如NetworkImage类和AssetImage类,NetworkImage是从网络来加载图片数据,而AssetImage则是从安装包里的资源文件中加载。

图片的加载

在ImageProvider中提供了一个load()方法,它是一个用于加载图片数据源的接口,不同的数据源的加载方式不同。
加载网络图片是用Image.network(),对应的ImageProvider是NetworkImage类,它实现了load()方法:

  
  ImageStreamCompleter load(FileImage key, DecoderCallback decode) {
    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, null, decode),
      scale: key.scale,
      debugLabel: key.file.path,
      informationCollector: () => <DiagnosticsNode>[
        ErrorDescription('Path: ${file.path}'),
      ],
    );
  }
  • load方法的返回值类型是ImageStreamCompleter,它是一个抽象类,定义了管理图片加载过程的一些接口,Image Widget中是通过它来监听图片加载状态的;
  • MultiFrameImageStreamCompleter是ImageStreamCompleter的一个子类,实现该类,可以快速的创建出一个ImageStreamCompleter实例;

MultiFrameImageSteamCompleter有个codec参数,源码中是用调用了_loadAsync()方法,方法的实现如下:

Future<ui.Codec> _loadAsync(
   NetworkImage key,
   StreamController<ImageChunkEvent> chunkEvents,
   image_provider.DecoderBufferCallback? decode,
   image_provider.DecoderCallback? decodeDepreacted,
   ) async {
 try {
   final Uri resolved = Uri.base.resolve(key.url);

   final HttpClientRequest request = await _httpClient.getUrl(resolved);

   headers?.forEach((String name, String value) {
     request.headers.add(name, value);
   });
   final HttpClientResponse response = await request.close();
   if (response.statusCode != HttpStatus.ok) {
     await response.drain<List<int>>(<int>[]);
     throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
   }

   final Uint8List bytes = await consolidateHttpClientResponseBytes(
     response,
     onBytesReceived: (int cumulative, int? total) {
       chunkEvents.add(ImageChunkEvent(
         cumulativeBytesLoaded: cumulative,
         expectedTotalBytes: total,
       ));
     },
   );
   ...
   if (decode != null) {
       final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
       return decode(buffer);
     } else {
       assert(decodeDepreacted != null);
       return decodeDepreacted!(bytes);
     }
}

通过源码可以发现_loadAsync()方法负责下载图片,并调用decode()方法对下载的图片数据进行解码。

图片的缓存

图片的缓存的关键方法是:obtainKey(ImageConfiguration)
该方法主要是为了配合实现图片缓存,ImageProvider从数据源加载完数据后,会在全局的ImageCache中缓存图片数据,而图片数据缓存是一个Map,而Map的key便是调用此方法的返回值,不同的key代表不同的图片数据缓存。
resolve方法是ImageProvider暴露给Image的主入口方法,它接收一个ImageConfiguration参数,返回ImageStream。

ImageStream resolve(ImageConfiguration configuration) {
  assert(configuration != null);
  final ImageStream stream = createStream(configuration);
  _createErrorHandlerAndKey(
    configuration,
        (T key, ImageErrorListener errorHandler) {
      resolveStreamForKey(configuration, stream, key, errorHandler);
    },
       ...
  );
  return stream;
}

ImageConfiguration:包含图片和设备的相关信息。内部会调用_createErrorHandlerAndKey来加载key并创建错误处理函数。
resolveStreamForKey方法:


void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
  if (stream.completer != null) {
    //缓存逻辑
    final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent(
      key,
          () => stream.completer!,
      onError: handleError,
    );
    assert(identical(completer, stream.completer));
    return;
  }
  //缓存逻辑
  final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent(
    key,
        () => loadBuffer(key, PaintingBinding.instance.instantiateImageCodecFromBuffer),
    onError: handleError,
  );
  if (completer != null) {
    stream.setCompleter(completer);
  }
}

在resolve方法中会调用resolveStreamForKey,其中PaintingBinding.instance.imageCache是ImageCache的一个实例,它是PaintingBinding的一个属性,并且PaintingBinding.instance和imageCache都是单例,所以图片缓存是全局的,统一由PaintingBinding.instance.imageCache来管理。

ImageCache缓存的具体实现

ImageCache的定义:

const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
class ImageCache {
  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};

  final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{};

  int get maximumSize => _maximumSize;
  int _maximumSize = _kDefaultSize;
  ...
}

ImageCache中有三个缓存池:

  • _pendingImages:用来存放正在加载和解码的图片,当图片加载和解码完成后,ImageCache会自动移除_pendingImages相应的Entry;
  • _cache:用来存储所有加载过的图片,如果图片缓存的数量和内存占用大小没有超过ImageCache的上限,_cache就会一直保留Cache Entry,如果超过了则会按LRU进行释放;
  • _liveImages:用来存放使用中的图片,当Image Widget移除或者更换图片,或者Image Widget自身被移除,ImageCache会从_liveImages移除相应Entry;
    只有ImageCache从所有缓存池都释放了同一张图片的Entry,该图片才算在内存中真正释放。

图片缓存的Key怎么生成

因为Map中相同key的值会被覆盖,也就是说key是图片缓存的唯一标识,只要key不同,那么图片数据就会分布缓存。那么图片的唯一标识是什么呢?从源码中可以看到ImageProvider.obtainKey()方法,图片在缓存时所使用的key就是通过这个方法生成的,并且ImageProvider的子类对这个方法进行了重写。
Flutter开发——图片加载与缓存源码解析_第1张图片
这就意味着不同的ImageProvider对key的定义逻辑是不同的。NetworkImage的obtainKey()方法:

  
  Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
    return SynchronousFuture<NetworkImage>(this);
  }

创建了一个SynchronousFuture,然后将自身返回,所以对比key的时候,看操作符==就可以了:

  
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is NetworkImage && other.url == url && other.scale == scale;
  }

对于网络图片来说,key实际上是url+缩放比例。所以两张图片的url和scale都相同,那么他们在内存里面就只会缓存一份。

设置缓存大小

ImageCache类中有默认的缓存大小:

const int _kDefaultSize = 1000;//最多1000张
const int _kDefaultSizeBytes = 100 << 20; //最大 100 MiB

我们也可以通过如下代码去设置自定义的缓存上限:

 PaintingBinding.instance.imageCache.maximumSize=500; //最多500张
 PaintingBinding.instance.imageCache.maximumSizeBytes = 50 << 20; //最大50M

你可能感兴趣的:(flutter,缓存,android)