打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Flutter图片加载与缓存机制的深入探究
目录

·        前言

·        图片控件

·        图片解析

·        缓存管理

·        新增缓存

·        缓存清理

·        图片加载

·        滑动中处理

·        总结

前言

今天来学习一下 Flutter 自身是如何加载图片和管理图片的。

Flutter 提供了一个图片控件 Image,Image 定义了若干中加载图片的方式,包括 Image.asset、Image.file、Image.network、Image.memory。

Image内部维护了一个 ImageProvider对象,ImageProvider则真正维护整个图片加载的工作。Widget 本身内部是体现在 RawImage中:

图片控件

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

// Image

Widget result = RawImage(

      image: _imageInfo?.image,

      debugImageLabel: _imageInfo?.debugLabel,

      width: widget.width,

      height: widget.height,

      scale: _imageInfo?.scale ?? 1.0,

      color: widget.color,

      colorBlendMode: widget.colorBlendMode,

      fit: widget.fit,

      alignment: widget.alignment,

      repeat: widget.repeat,

      centerSlice: widget.centerSlice,

      matchTextDirection: widget.matchTextDirection,

      invertColors: _invertColors,

      isAntiAlias: widget.isAntiAlias,

      filterQuality: widget.filterQuality,

    );

return result;

这里可以看到 _imageInfo 决定 RawImage如何展示图片。

_imageInfo 则会在图片的每一帧进行重新赋值:

1

2

3

4

5

6

// image.dart

void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {

    setState(() {

    _imageInfo = imageInfo;

  }

}

那么图片信息是从哪里来的呢,它是由 _resolveImage 这个方法发起的。这个方法会在 _ImageState 的 didChangeDependencies、 didUpdateWidget和 reassemble方法进行调用。

也就是控件发生变化刷新状态的时候,就会重新去解析图片。

图片解析

_resolveImage 逻辑如下:

1

2

3

4

5

6

7

8

9

10

11

12

void _resolveImage() {

    final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(

      context: _scrollAwareContext,

      imageProvider: widget.image,

    );

    final ImageStream newStream =

      provider.resolve(createLocalImageConfiguration(

        context,

        size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,

      ));

    _updateSourceStream(newStream);

}

这里会用 ScrollAwareImageProvider 包装一下,ScrollAwareImageProvider的功能我们后面会介绍,这里先跳过。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

//ImageProvider# resolve

ImageStream resolve(ImageConfiguration configuration) {

    _createErrorHandlerAndKey(configuration,(T key, ImageErrorListener errorHandler) {

        resolveStreamForKey(configuration, stream, key, errorHandler);

      },

        (T? key, dynamic 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,

        );

      }

    );

}

resolve 方法调用 _createErrorHandlerAndKey来处理图片加载的异常情况。当图片正常加载的时候,会执行 resolveStreamForKey。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

//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,

      );

        return;

    }

    final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(

        key,

      () => load(key, PaintingBinding.instance!.instantiateImageCodec),

      onError: handleError,

        );

    if (completer != null) {

      stream.setCompleter(completer);

    }

}

Flutter 会把图片缓存相关的逻辑维护在 ImageCache这个对象。

缓存管理

ImageCache里面有 3 个 map:

分别表示

·        正在加载的图片

·        缓存在内存的图片

·        表示正活跃的图片,Widget 状态变化后可能会清空

新增缓存

新增缓存的时候会设置 map 的 key, key 由 ImageProvider 对象提供。例如:

·        AssetImage  当包名和bundle一样的时候,key可以认为是一样的。

·        NetworkImage 当图片 url 和比例一样的时候,key可以认为是一样的。

ImageCache 实际上是一个单例对象。也就是 Flutter 的图片缓存管理是全局的。ImageCache 最重要的方法就是 putIfAbsent:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

// 整理过核心逻辑的代码

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

  // 根据key从正在加载的map里获取缓存,如果有直接返回

    ImageStreamCompleter? result = _pendingImages[key]?.completer;

    if (result != null) {

      return result;

    }

  // 检查内存缓存,存在的话更新存活map

  final _CachedImage? image = _cache.remove(key);

  if (image != null) {

    _trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));

    _cache[key] = image;

    return image.completer;

  }

  // 没有缓存,从 _live 里面取

  final _CachedImage? liveImage = _liveImages[key];

  if (liveImage != null) {

    // 更新缓存

    _touch(key, liveImage, timelineTask);

    return liveImage.completer;

  }

  // 3 个 map 都没有获取到缓存的图片

  result = loader(); // 加载

  _trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));

    _PendingImage? untrackedPendingImage;

  //定义一个listener

    void listener(ImageInfo? info, bool syncCall) {

        // 加载的监听

    }

  // 包装一个listener

    final ImageStreamListener streamListener = ImageStreamListener(listener);

    if (maximumSize > 0 && maximumSizeBytes > 0) {

        // 放入缓存

        _pendingImages[key] = _PendingImage(result, streamListener);

    } else {

        untrackedPendingImage = _PendingImage(result, streamListener);

    }

    // 添加监听

    result.addListener(streamListener);

    return result;

}

listener 回调的逻辑:

在 Image 状态改变的时候,会触发对 liveImages 的修改:

1

2

3

4

5

6

7

8

9

10

// Image

_imageStream.removeListener(_getListener());

// ImageStream

void removeListener(ImageStreamListener listener) {

  for (final VoidCallback callback in _onLastListenerRemovedCallbacks) {

    callback();

  }

  _onLastListenerRemovedCallbacks.clear();

}

而在 _trackLiveImage 的时候,_LiveImage 都注册了上面的这个 callback:

1

_trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));

这时候改图片会从 _liveImages 里面移除。

由此可见,缓存的优先级为 pending -> cache -> live -> load,图片缓存和获取的流程如下图所示:

缓存清理

在更新缓存大小的时候,还会进行缓存大小的检查:

1

2

3

4

5

6

7

8

void _checkCacheSize(TimelineTask? timelineTask) {

  while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {

    final Object key = _cache.keys.first;

    final _CachedImage image = _cache[key]!;

    _currentSizeBytes -= image.sizeBytes!;

    _cache.remove(key);

  }

}

当当前缓存总容量大于最大容量或者缓存数量大于最大数量的时候,就会进行缓存的清理。

所以上面使用缓存的过程中,多次访问的缓存就会把key往后放,避免一上来就被清理掉。

所以 Flutter 自身的缓存清理算法也是遵循了 “最近最少使用” 的。

图片缓存的逻辑如下图所示:

图片加载

图片加载主要依赖上面的 load方法进行。不同的 ImageProvider 子类有自己的实现。例如

AssetImage

1

2

3

4

5

6

return MultiFrameImageStreamCompleter(

      codec: _loadAsync(key, decode),

      scale: key.scale,

      debugLabel: key.name,

      informationCollector: collector

    );

NetworkImage

1

2

3

4

5

6

7

8

9

final StreamController<ImageChunkEvent> chunkEvents =

        StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(

        chunkEvents: chunkEvents.stream,

        codec: _loadAsync(key as NetworkImage, decode, chunkEvents),

        scale: key.scale,

        debugLabel: key.url,

        informationCollector: _imageStreamInformationCollector(key));

逻辑基本一样,具体特异的流程体现在 loadAsync里面:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

// AssetImage _loadAsync

try {

      data = await key.bundle.load(key.name);

    } on FlutterError {

      PaintingBinding.instance!.imageCache!.evict(key);

      rethrow;

    }

if (data == null) {

// 加载数据是null,清掉这个key的缓存

    PaintingBinding.instance!.imageCache!.evict(key);

    throw StateError('Unable to read data');

}

return await decode(data.buffer.asUint8List());

/// NetworkImage _loadAsync

Future<ui.Codec> _loadAsync(

      NetworkImage key,

      image_provider.DecoderCallback decode,

      StreamController<ImageChunkEvent> chunkEvents) {

    final Uri resolved = Uri.base.resolve(key.url);

    return ui.webOnlyInstantiateImageCodecFromUrl(resolved, // ignore: undefined_function

        chunkCallback: (int bytes, int total) {

      chunkEvents.add(ImageChunkEvent(

          cumulativeBytesLoaded: bytes, expectedTotalBytes: total));

    }) as Future<ui.Codec>;

}

这里分别会从 bundle 里加载图片和从网络拉取图片。

滑动中处理

还记得上面提到的 ScrollAwareImageProvider吗,这里会有一个关于滑动中的判断:

1

2

3

4

5

6

if (Scrollable.recommendDeferredLoadingForContext(context.context)) {

  SchedulerBinding.instance.scheduleFrameCallback((_) {

        scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));

     });

    return;

}

当 if 里的逻辑成立,就把解析图片的工作放到下一帧。recommendDeferredLoadingForContext的具体逻辑:

1

2

3

4

5

6

7

8

9

10

static bool recommendDeferredLoadingForContext(BuildContext context) {

    final _ScrollableScope widget =

        context.getElementForInheritedWidgetOfExactType<_ScrollableScope>()?.widget as _ScrollableScope;

    if (widget == null) {

      return false;

    }

    // 存在滑动的widget

    return widget.position.recommendDeferredLoading(context);

}

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
译文:理解Java中的弱引用
Flutter 一些常用第三方库、插件
图片缓存之内存缓存技术LruCache,软引用
一起写一个Android图片加载框架
Android之使用Android
图片加载框架Universal-Image-Loader源码解析
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服