美文网首页Flutter
Flutter图片加载与缓存

Flutter图片加载与缓存

作者: xmb | 来源:发表于2021-07-19 18:04 被阅读0次

一、是谁加载了图片

Image构造方法:
  const Image({
    Key? key,
    required this.image,
    this.frameBuilder,
    this.loadingBuilder,
    this.errorBuilder,
    this.semanticLabel,
    this.excludeFromSemantics = false,
    this.width,
    this.height,
    this.color,
    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,
  }) : assert(image != null),
       assert(alignment != null),
       assert(repeat != null),
       assert(filterQuality != null),
       assert(matchTextDirection != null),
       assert(isAntiAlias != null),
       super(key: key);

其中,参数image类型为抽象类ImageProvider,定义了图片数据获取和加载的相关接口。

  /// The image to display.
  final ImageProvider image;
ImageProvider作用:
  1. 提供图片数据源
  2. 缓存图片
ImageProvider定义:
abstract class ImageProvider<T extends Object> {

  const ImageProvider();

  ImageStream resolve(ImageConfiguration configuration) {
    ...
  }

  /// Evicts an entry from the image cache.
  Future<bool> evict({ ImageCache? cache, ImageConfiguration 
    ...
  }

  @protected
  ImageStreamCompleter load(T key, DecoderCallback decode);

  @override
  String toString() => '${objectRuntimeType(this, 'ImageConfiguration')}()';
}
image.png
ImageProvider派生类

根据不同的数据来源,派生出不同的ImageProvider

// 本地项目包中的图片
abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKey> {}
class AssetImage extends AssetBundleImageProvider {}

// 对图片进行宽高处理的图片
class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {}

// 网络图片
abstract class NetworkImage extends ImageProvider<NetworkImage> {}
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {}

// 本地图片文件
class FileImage extends ImageProvider<FileImage> {}

// 内存图片
class MemoryImage extends ImageProvider<MemoryImage> {}

二、图片如何加载

抽象类ImageProvider提供了一个用于加载数据源的抽象方法@protected ImageStreamCompleter load(T key, DecoderCallback decode);接口,不同的数据源定义各自的实现。

子类NetworkImage实现如下:

  @override
  ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
    // Ownership of this controller is handed off to [_loadAsync]; it is that
    // method's responsibility to close the controller's stream when the image
    // has been loaded or an error is thrown.
    final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
      chunkEvents: chunkEvents.stream,
      scale: key.scale,
      debugLabel: key.url,
      informationCollector: () {
        return <DiagnosticsNode>[
          DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
          DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
        ];
      },
    );
  }

load方法返回类型为抽象类ImageStreamCompleter,其中定义了一些管理图片加载过程的接口,比如addListenerremoveListeneraddOnLastListenerRemovedCallback等,MultiFrameImageStreamCompleter为其子类。

MultiFrameImageStreamCompleter第一个参数codec类型为Future<ui.Codec>,用来对突破进行解码,当codec准备好的时候,就会立即对图片第一帧进行解码操作。

  /// Immediately starts decoding the first image frame when the codec is ready.
  ///
  /// The `codec` parameter is a future for an initialized [ui.Codec] that will
  /// be used to decode the image.

codec_loadAsync方法返回值,

  Future<ui.Codec> _loadAsync(
    NetworkImage key,
    StreamController<ImageChunkEvent> chunkEvents,
    image_provider.DecoderCallback decode,
  ) async {
    try {
      assert(key == this);

      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) {
        // The network may be only temporarily unavailable, or the file will be
        // added on the server later. Avoid having future calls to resolve
        // fail to check the network again.
        await response.drain<List<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 (bytes.lengthInBytes == 0)
        throw Exception('NetworkImage is an empty file: $resolved');

      return decode(bytes);
    } catch (e) {
      // Depending on where the exception was thrown, the image cache may not
      // have had a chance to track the key in the cache at all.
      // Schedule a microtask to give the cache a chance to add the key.
      scheduleMicrotask(() {
        PaintingBinding.instance!.imageCache!.evict(key);
      });
      rethrow;
    } finally {
      chunkEvents.close();
    }
  }

_loadAsync方法实现:

  1. 通过图片url网络请求获取图片的返回值Response,将返回值转为Uint8List
  2. 调用decode解码方法进行返回,传入Uint8List,返回Future<ui.Codec>

三、图片如何进行解码

decode方法的类型:
其中解码传入的回调方法image_provider.DecoderCallback decode

typedef DecoderCallback = Future<ui.Codec> Function(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool allowUpscaling});

传入Uint8List,返回Future<ui.Codec>

而对decode回调方法的具体定义,在ImageProviderresolveStreamForKey方法中做了定义,resolveStreamForKey方法在ImageProviderresolve方法中有调用,resolve方法则为ImageProvider类层级结构的公共入口点。

resolveStreamForKeyresolve实现如下:

  @nonVirtual
  ImageStream resolve(ImageConfiguration configuration) {
    assert(configuration != null);
    final ImageStream stream = createStream(configuration);
    // Load the key (potentially asynchronously), set up an error handling zone,
    // and call resolveStreamForKey.
    _createErrorHandlerAndKey(
      configuration,
      (T key, ImageErrorListener errorHandler) {
        resolveStreamForKey(configuration, stream, key, errorHandler);
      },
      (T? key, Object exception, StackTrace? stack) async {
        await null; // wait an event turn in case a listener has been added to the image stream.
        final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
        stream.setCompleter(imageCompleter);
        InformationCollector? collector;
        assert(() {
          collector = () sync* {
            yield DiagnosticsProperty<ImageProvider>('Image provider', this);
            yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
            yield DiagnosticsProperty<T>('Image key', key, defaultValue: null);
          };
          return true;
        }());
        imageCompleter.setError(
          exception: exception,
          stack: stack,
          context: ErrorDescription('while resolving an image'),
          silent: true, // could be a network error or whatnot
          informationCollector: collector,
        );
      },
    );
    return stream;
  }

  @protected
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
    // This is an unusual edge case where someone has told us that they found
    // the image we want before getting to this method. We should avoid calling
    // load again, but still update the image cache with LRU information.
    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,
      () => load(key, PaintingBinding.instance!.instantiateImageCodec),
      onError: handleError,
    );
    if (completer != null) {
      stream.setCompleter(completer);
    }
  }

decode方法,即PaintingBinding.instance!.instantiateImageCodec,即为具体图片解码的方法实现。

  Future<ui.Codec> instantiateImageCodec(Uint8List bytes, {
    int? cacheWidth,
    int? cacheHeight,
    bool allowUpscaling = false,
  }) {
    assert(cacheWidth == null || cacheWidth > 0);
    assert(cacheHeight == null || cacheHeight > 0);
    assert(allowUpscaling != null);
    return ui.instantiateImageCodec(
      bytes,
      targetWidth: cacheWidth,
      targetHeight: cacheHeight,
      allowUpscaling: allowUpscaling,
    );
  }

ui.instantiateImageCodec实现:

Future<Codec> instantiateImageCodec(
  Uint8List list, {
  int? targetWidth,
  int? targetHeight,
  bool allowUpscaling = true,
}) async {
  final ImmutableBuffer buffer = await ImmutableBuffer.fromUint8List(list);
  final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer);
  if (!allowUpscaling) {
    if (targetWidth != null && targetWidth > descriptor.width) {
      targetWidth = descriptor.width;
    }
    if (targetHeight != null && targetHeight > descriptor.height) {
      targetHeight = descriptor.height;
    }
  }
  return descriptor.instantiateCodec(
    targetWidth: targetWidth,
    targetHeight: targetHeight,
  );
}

descriptor.instantiateCodec方法实现:

  Future<Codec> instantiateCodec({int? targetWidth, int? targetHeight}) async {
    if (targetWidth != null && targetWidth <= 0) {
      targetWidth = null;
    }
    if (targetHeight != null && targetHeight <= 0) {
      targetHeight = null;
    }

    if (targetWidth == null && targetHeight == null) {
      targetWidth = width;
      targetHeight = height;
    } else if (targetWidth == null && targetHeight != null) {
      targetWidth = (targetHeight * (width / height)).round();
      targetHeight = targetHeight;
    } else if (targetHeight == null && targetWidth != null) {
      targetWidth = targetWidth;
      targetHeight = targetWidth ~/ (width / height);
    }
    assert(targetWidth != null);
    assert(targetHeight != null);

    final Codec codec = Codec._();
    _instantiateCodec(codec, targetWidth!, targetHeight!);
    return codec;
  }

_instantiateCodec方法的实现,最终到了native的实现:

  void _instantiateCodec(Codec outCodec, int targetWidth, int targetHeight) native 'ImageDescriptor_instantiateCodec';

其中返回值类型Codec里定义了一些属性:

class Codec extends NativeFieldWrapperClass2 {

  @pragma('vm:entry-point')
  Codec._();

  // 缓存帧的数量
  int? _cachedFrameCount;

  // 获取图片帧数
  int get frameCount => _cachedFrameCount ??= _frameCount;
  int get _frameCount native 'Codec_frameCount';

  int? _cachedRepetitionCount;
  
  // 动画重复的次数
  int get repetitionCount => _cachedRepetitionCount ??= _repetitionCount;
  int get _repetitionCount native 'Codec_repetitionCount';

  // 获取下一帧
  Future<FrameInfo> getNextFrame() async {
    ...
  }

  /// Returns an error message on failure, null on success.
  String? _getNextFrame(void Function(_Image?, int) callback) native 'Codec_getNextFrame';

  void dispose() native 'Codec_dispose';
}

四、图片如何缓存

obtainKey方法:
ImageProvider定义了一个抽象方法Future<T> obtainKey(ImageConfiguration configuration);,供子类来实现,其中NetworkImage的实现为:

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

obtainKey作用:
配合实现图片缓存,ImageProvider从数据源加载完数据后,会在ImageCache中缓存图片数据,图片数据缓存时一个Map,其中Map中的key便是obtainKey

resolve作为ImageProvider提供给Image的主入口方法,参数为ImageConfiguration

  const ImageConfiguration({
    this.bundle,
    this.devicePixelRatio, // 设备像素比
    this.locale,
    this.textDirection,
    this.size, // 图片大小
    this.platform, // 设备平台
  });

resolve其中调用了_createErrorHandlerAndKey方法,设置了成功回调和失败回调:

  @nonVirtual
  ImageStream resolve(ImageConfiguration configuration) {
    assert(configuration != null);
    final ImageStream stream = createStream(configuration);
    // Load the key (potentially asynchronously), set up an error handling zone,
    // and call resolveStreamForKey.
    _createErrorHandlerAndKey(
      configuration,
      (T key, ImageErrorListener errorHandler) {
        // 设置成功回调
        resolveStreamForKey(configuration, stream, key, errorHandler);
      },
      (T? key, Object exception, StackTrace? stack) async {
        // 设置失败回调
        await null; // wait an event turn in case a listener has been added to the image stream.
        final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
        stream.setCompleter(imageCompleter);
        InformationCollector? collector;
        assert(() {
          collector = () sync* {
            yield DiagnosticsProperty<ImageProvider>('Image provider', this);
            yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
            yield DiagnosticsProperty<T>('Image key', key, defaultValue: null);
          };
          return true;
        }());
        imageCompleter.setError(
          exception: exception,
          stack: stack,
          context: ErrorDescription('while resolving an image'),
          silent: true, // could be a network error or whatnot
          informationCollector: collector,
        );
      },
    );
    return stream;
  }

其中_createErrorHandlerAndKey方法的实现,便调用了obtainKey来设置key

  void _createErrorHandlerAndKey(
    ImageConfiguration configuration,
    _KeyAndErrorHandlerCallback<T> successCallback,
    _AsyncKeyErrorHandler<T?> errorCallback,
  ) {
    T? obtainedKey;
    bool didError = false;
    Future<void> handleError(Object exception, StackTrace? stack) async {
      if (didError) {
        return;
      }
      if (!didError) {
        errorCallback(obtainedKey, exception, stack);
      }
      didError = true;
    }

    // If an error is added to a synchronous completer before a listener has been
    // added, it can throw an error both into the zone and up the stack. Thus, it
    // looks like the error has been caught, but it is in fact also bubbling to the
    // zone. Since we cannot prevent all usage of Completer.sync here, or rather
    // that changing them would be too breaking, we instead hook into the same
    // zone mechanism to intercept the uncaught error and deliver it to the
    // image stream's error handler. Note that these errors may be duplicated,
    // hence the need for the `didError` flag.
    final Zone dangerZone = Zone.current.fork(
      specification: ZoneSpecification(
        handleUncaughtError: (Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) {
          handleError(error, stackTrace);
        },
      ),
    );
    dangerZone.runGuarded(() {
      Future<T> key;
      try {
        key = obtainKey(configuration);
      } catch (error, stackTrace) {
        handleError(error, stackTrace);
        return;
      }
      key.then<void>((T key) {
        obtainedKey = key;
        try {
          successCallback(key, handleError);
        } catch (error, stackTrace) {
          handleError(error, stackTrace);
        }
      }).catchError(handleError);
    });
  }

在成功回调里,调用了方法resolveStreamForKey,里面有具体的缓存实现PaintingBinding.instance!.imageCache!.putIfAbsent

  @protected
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
    // This is an unusual edge case where someone has told us that they found
    // the image we want before getting to this method. We should avoid calling
    // load again, but still update the image cache with LRU information.
    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,
      () => load(key, PaintingBinding.instance!.instantiateImageCodec),
      onError: handleError,
    );
    if (completer != null) {
      stream.setCompleter(completer);
    }
  }

PaintingBinding.instance!.imageCache是ImageCache的一个实例,是PaintingBinding的一个属性,是一个单例,图片缓存是全局的。
如上述判断:

  1. 先判断图片缓存数据有没有缓存,如果有,直接返回ImageStream
  2. 如果没有缓存,则调用load方法总数据源加载图片数据,然后返回ImageStream

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;

    // 设置缓存最大数量
  set maximumSize(int value) {
    ...
  }
  
  // 当前缓存数量
  int get currentSize => _cache.length;

  // 获取最大缓存容量
  int get maximumSizeBytes => _maximumSizeBytes;
  int _maximumSizeBytes = _kDefaultSizeBytes;

  // 设置缓存最大容量
  set maximumSizeBytes(int value) {
    ...
  }
  
  // 当前缓存容量
  int get currentSizeBytes => _currentSizeBytes;
  int _currentSizeBytes = 0;

  // 清除缓存
  void clear() {
    ...
  }

  bool evict(Object key, { bool includeLive = true }) {
    ...
  }

  ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {
    ...
  }

  ImageCacheStatus statusForKey(Object key) {
    ...
  }

  bool containsKey(Object key) {
    ...
  }
  
  // 正在使用中的图片数量
  int get liveImageCount => _liveImages.length;
  
  // 正在加载中的图片数量
  int get pendingImageCount => _pendingImages.length;
  
  // 清空正在使用中的图片
  void clearLiveImages() {
    ...
  }

  void _checkCacheSize(TimelineTask? timelineTask) {
    ...
  }
}

ImageCache缓存池:

  1. _pendingImages:正在加载的图片缓存池,图片解码完成后会自动移除_pendingImages中对应的缓存Entry
  2. _cache:已经缓存的所有图片的缓存池,如果缓存的图片数量和缓存容量没有超出设置的最大数量和容量,则_cache会一直保留缓存Entry,超出则按照图片url进行释放。
  3. _liveImages:跟踪正在使用的图片的缓存池,如果Image widget被移除或更换图片,则会从_liveImages中移除缓存Entry

只有一个图片的缓存Entry从三个缓存池中都释放,图片解码后生成的纹理内存才会被真正释放。

五、图片缓存key的生成

NetworkImage中,对ImageProvider的抽象方法obtainKey进行了实现,将自己创建了一个同步Future进行返回:

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

同时,自身又重写了ImageProvider定义的==比较操作符,通过图片url和图片的缩放比例scale进行比较:

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

提示:在Flutter框架中内置的图片缓存实现都是基于内存的,并没有进行本地文件持久化存储,应用重启后需要重新通过网络下载。

六、设置缓存大小

通过ImageCache提供的方法来设置:

PaintingBinding.instance!.imageCache. maximumSize = 2000; // 最多2000张
PaintingBinding.instance!.imageCache. maximumSizeBytes = 200 << 20; // 最大200M

相关文章

  • Flutter图片加载原理与缓存

    图片加载原理与缓存 在本书前面章节已经介绍过Image 组件,并提到Flutter框架对加载过的图片是有缓存的(内...

  • Flutter图片加载与缓存

    一、是谁加载了图片 Image构造方法: 其中,参数image类型为抽象类ImageProvider,定义了图片数...

  • 初探Flutter

    Flutter中的图片加载 Flutter中加载远程图片相对比较容易简单,如下: 想要在Flutter中加载本地图...

  • SDWebImage

    主要功能 为UIImageView提供一个分类,支持网络图片的加载与缓存异步图片加载器异步内存+磁盘图片缓存支持G...

  • Flutter学习

    中文文档 官方简单布局 demo flutter - 加载本地图片、网络图片图片加载适配 flutter - Ex...

  • Flutter Image.network图片加载原理分析

    Flutter加载图片与Glide[https://juejin.cn/post/6850037273644236...

  • 图片的预加载与懒加载

    图片预加载与懒加载 由名字可以知道,图片的预加载->当用户需要查看图片可以直接从本地缓存中取到(提前加载下来的),...

  • 2018-12-27

    flutter加载本地图片加载不出来 flutter Image 加载图片可以分为4中情况,分别为: Image....

  • 图片缓存

    图片缓存加载方法

  • 基础模块封装 -- 图片加载

    一、图片加载管理类 二、图片加载封装类 三、图片大小封装类 四、内存缓存策略类 五、磁盘缓存策略类 六、图片加载回调类

网友评论

    本文标题:Flutter图片加载与缓存

    本文链接:https://www.haomeiwen.com/subject/emzcmltx.html