Flutter

Flutter系列之Image加载原理

2020-09-16  本文已影响0人  XQSY

一、前言

最近在做的项目中,总是用到Image组件,所以就了解了一下Image的源码,顺便记录下来,和大家分享一下。
本文是基于1.12.13+hotfix.8的源码,以加载网路图片为例进行解读。毕竟自己还是个小白,如果有解读不对的地方,欢迎指正。

二、Image

Image继承了StatefulWidget,是用于显示图片的 Widget,最后通过内部的 RenderImage 绘制。
先看看Image结构,以Image.network为例:


image.png

先简单介绍一下这些类,后续我们会一一详细介绍。

下面我们开始看下源码。

构造函数
Image.network(
    String src,{
    Key key,
    @required this.image,
    this.frameBuilder,
    this.loadingBuilder,
    ...
    this.filterQuality = FilterQuality.low,
  }): image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
   // ...
    super(key: key);

Image.network以命名构造函数创建Image对象时,会同时初始化实例变量image。

//ImageProvider初始化
class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {
   ...
  static ImageProvider<dynamic> resizeIfNeeded(int cacheWidth, int cacheHeight, ImageProvider<dynamic> provider) {
    if (cacheWidth != null || cacheHeight != null) {
      return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
    }
    return provider;
  }
}
State

作为一个StatefulWidget,最重要的当然是State了。

@override
  _ImageState createState() => _ImageState();

Image的主要构成就是两部分,\color{#FF0000}{ImageProvider}\color{#FF0000}{ImageState}
接下来我们分别介绍一下这两部分。

三、_ImageState

Image是一个StatefulWidget,状态由_ImageState控制。_ImageState继承自State,其生命周期方法包括initState()、didChangeDependencies()、build()、dispose()、didUpdateWidget()等。我们先来看看_ImageState中都做了些什么。

成员变量
class _ImageState extends State<Image> with WidgetsBindingObserver {  
  ImageStream _imageStream; 
  ImageInfo _imageInfo;
  bool _isListeningToStream = false;
  ···
}
生命周期函数
 @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);//监听生命周期
  }
 @override
  void didChangeDependencies() {
    ...
    _resolveImage();

    if (TickerMode.of(context))
      _listenToStream();
    else
      _stopListeningToStream();

    super.didChangeDependencies();
  }

_resolveImage()方法是核心,我们来分析一下。

  void _resolveImage() {
    final ImageStream newStream =
      widget.image.resolve(createLocalImageConfiguration(
        context,
        size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
      ));
    assert(newStream != null);
    _updateSourceStream(newStream);
  }

 void _updateSourceStream(ImageStream newStream) {
    if (_imageStream?.key == newStream?.key)
      return;

    if (_isListeningToStream)
      _imageStream.removeListener(_getListener());

    if (!widget.gaplessPlayback)
      setState(() { _imageInfo = null; });

    setState(() {
      _loadingProgress = null;
      _frameNumber = null;
      _wasSynchronouslyLoaded = false;
    });

    _imageStream = newStream;
    if (_isListeningToStream)
      _imageStream.addListener(_getListener());
  }

 ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
      loadingBuilder ??= widget.loadingBuilder;
      return ImageStreamListener(
        _handleImageFrame,
        onChunk: loadingBuilder == null ? null : _handleImageChunk,
    );
  }

1、 通过ImageProvider得到ImageStream 对象
2、 然后 _ImageState 利用 ImageStream 添加监听,等待图片数据

 @override
  void didUpdateWidget(Image oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (_isListeningToStream &&
        (widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) {
      _imageStream.removeListener(_getListener(oldWidget.loadingBuilder));
      _imageStream.addListener(_getListener());
    }
    if (widget.image != oldWidget.image)
      _resolveImage();
  }
 @override
  Widget build(BuildContext context) {
    Widget result = RawImage(
         image: _imageInfo?.image,
         ...
    );

    if (!widget.excludeFromSemantics) {
      result = Semantics(
       ...
      );
    }
    ...
    return result;
  }

四、ImageProvider

ImageProvider是一个抽象类,提供图片数据获取和加载的的接口,NetworkImage 、AssetImage 等均实现了这个接口。
它主要有两个功能:

abstract class ImageProvider<T> {
  //接收ImageConfiguration参数,返回ImageStream-图片数据流
  ImageStream resolve(ImageConfiguration configuration) {
   ...
  }
  //清除指定key对应的图片缓存
  Future<bool> evict({ ImageCache cache,ImageConfiguration configuration = ImageConfiguration.empty }) async {
   ...
  }
 //需要ImageProvider子类实现,不同的ImageProvider对key的定义逻辑不同
  Future<T> obtainKey(ImageConfiguration configuration); 
 // 需ImageProvider子类实现,加载图片数据
  @protected
  ImageStreamCompleter load(T key); 
}

4.1 resolve方法解析

#ImageProvider
ImageStream resolve(ImageConfiguration configuration) {
  //1、创建图片数据流
  final ImageStream stream = ImageStream();
  T obtainedKey; //
  //2、错误处理
  Future<void> handleError(dynamic exception, StackTrace stack) async {
    ... 
    stream.setCompleter(imageCompleter);
    imageCompleter.setError(...);
  }
   //3、创建一个新Zone,用来处理发生的错误,不干扰MainZone
    final Zone dangerZone = Zone.current.fork(
      specification: ZoneSpecification(
        handleUncaughtError: (Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) {
          handleError(error, stackTrace);
        }
      )
    );
    dangerZone.runGuarded(() {
      // 4、判断是否有缓存的相关逻辑
      Future<T> key;
      try {
        // 5、生成key,后续会用此key判断是否有缓存
        key = obtainKey(configuration);
      } catch (error, stackTrace) {
        handleError(error, stackTrace);
        return;
      }
      key.then<void>((T key) {
        // 6、缓存处理逻辑
        obtainedKey = key;
        final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
          key,
          () => load(key, PaintingBinding.instance.instantiateImageCodec),
          onError: handleError,
        );
        if (completer != null) {
          //7、stream设置ImageStreamCompleter对象
          stream.setCompleter(completer);
        }
      }).catchError(handleError);
    });
    return stream;
  }

这段代码中,我们需要重点看四个点,

ImageStream

存储ImageStreamCompleter,监听图片加载结果。

ImageCache

在resolve 方法中调用了PaintingBinding.instance.imageCache.putIfAbsent方法(注释6处),这里的PaintingBinding.instance.imageCache 是 ImageCache的一个实例。PaintingBinding.instance和imageCache是单例的,所以说图片缓存是项目全局的。

const int _kDefaultSize = 1000;// 最大缓存数量,默认1000
const int _kDefaultSizeBytes = 100 << 20;   // 最大缓存容量,默认100 MB
class ImageCache {
  // 正在加载中的图片队列
  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
  // 缓存队列
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
  // 最大缓存数量,默认1000
  int _maximumSize = _kDefaultSize;
  // 最大缓存容量,默认100 MB
  int _maximumSizeBytes = _kDefaultSizeBytes;
  ... // 省略部分代码
  // 清除全部缓存
  void clear() {
     ...
  }
  // 根据key清楚缓存
  bool evict(Object key) {
   // ...省略代码
  }
  //重点方法
  // 参数 key用来获取缓存,loader()加载回调方法,onError加载失败回调
  ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
   //_pendingImage 用于标示该key的图片处于加载中的状态 
    ImageStreamCompleter result = _pendingImages[key]?.completer;
    // 图片还未加载成功,直接返回
    if (result != null)
      return result;
    // 先移除缓存,拿到移除的缓存对象
    final _CachedImage image = _cache.remove(key);
    //把最近一次使用过的缓存在_map中
    if (image != null) {
      _cache[key] = image;
      return image.completer;
    }
   //没有缓存,使用loader()方法加载
    try {
      result = loader();
    } catch (error, stackTrace) {
      ...
    }
    void listener(ImageInfo info, bool syncCall) {
      final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
      final _CachedImage image = _CachedImage(result, imageSize);
      // 缓存处理的逻辑
      if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
        _maximumSizeBytes = imageSize + 1000;
      }
      _currentSizeBytes += imageSize;
      final _PendingImage pendingImage = _pendingImages.remove(key);
      if (pendingImage != null) {
        pendingImage.removeListener();
      }

      _cache[key] = image;
      _checkCacheSize();
    }
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      final ImageStreamListener streamListener = ImageStreamListener(listener);
      _pendingImages[key] = _PendingImage(result, streamListener);
      // Listener is removed in [_PendingImage.removeListener].
      result.addListener(streamListener);
    }
    return result;
  }

  // 当超过缓存最大数量或最大缓存容量,调用此方法清理到缓存,保持着最大数量和容量
  void _checkCacheSize() {
   while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
      final Object key = _cache.keys.first;
      final _CachedImage image = _cache[key];
      _currentSizeBytes -= image.sizeBytes;
      _cache.remove(key);
    }
    ... 
  }
}

putIfAbsent方法主要是先通过 key 判断内存中正在缓存的对象或者是否有缓存,如果有就返回该对象的ImageStreamCompleter ,否则就调用 loader 去加载并返回ImageStreamCompleter。
这里提醒大家两个地方:

ImageStreamCompleter

putIfAbsent的返回值返回了ImageStreamCompleter,而resolve方法中,最后调用了ImageStream的setCompleter的方法,给ImageStream设置一个ImageStreamCompleter对象。

  #ImageStream
  void setCompleter(ImageStreamCompleter value) {
    assert(_completer == null);
    _completer = value;
    if (_listeners != null) {
      final List<ImageStreamListener> initialListeners = _listeners;
      _listeners = null;
      initialListeners.forEach(_completer.addListener);
    }
  }

ImageStreamCompleter是一个抽象类,定义了管理图片加载过程的一些接口,Image Widget中正是通过它来监听图片加载状态的。每一个ImageStream对象只能设置一次,ImageStreamCompleter是为了辅助ImageStream解析和管理Image图片帧的,并且判断是否有初始化监听器,可以做一些初始化回调工作。

abstract class ImageStreamCompleter extends Diagnosticable {
  final List<_ImageListenerPair> _listeners = <_ImageListenerPair>[];
  ImageInfo _currentImage;
  FlutterErrorDetails _currentError;
  void addListener(ImageListener listener, { ImageErrorListener onError }) {...}
  void removeListener(ImageListener listener) {... }
  void reportError(...) {... }
  @protected
  void setImage(ImageInfo image) {
    _currentImage = image;
    if (_listeners.isEmpty)
      return;
    // Make a copy to allow for concurrent modification.
    final List<ImageStreamListener> localListeners = List<ImageStreamListener>.from(_listeners);
    for (ImageStreamListener listener in localListeners) {
      try {
        listener.onImage(image, false);
      } catch (exception, stack) {
        reportError(
         ...
        );
 }}}}

4.2 obtainKey

key是图片缓存的一个唯一标识,也是判断该图片是否应该被缓存的唯一条件。这个key就是ImageProvider.obtainKey()方法的返回值,不同类型的ImageProvider对key的定义逻辑会不同,所以此方法需要ImageProvider子类去重写。我们以NetworkImage为例,看一下它的obtainKey()实现:

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

其实就是创建一个future,然后将NetworkImage自身做为key返回。
那么又是如何判断key是否相等的呢?

 #NetworkImage
 @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final NetworkImage typedOther = other;
    return url == typedOther.url
        && scale == typedOther.scale;
  }

在NetworkImage中,是将url+ scale(缩放比例)作为缓存中的key。只有url和scale相等,才算是有缓存。也就是说如果两张图片的url或scale只要有一个不同,便会重新下载并分别缓存。

4.3 load(T key)方法解析

load()是ImageProvider加载图片数据源的接口,不同ImageProvider的数据源加载方法不同,每个ImageProvider的子类必须实现它。比如NetworkImage类和AssetImage类,它们都是ImageProvider的子类,NetworkImage是从网络来加载图片数据,AssetImage则是从最终的应用包里来加载。
我们以NetworkImage为例,看看其load方法的实现:

#NetworkImage
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {

  final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

  return MultiFrameImageStreamCompleter(
    codec: _loadAsync(key, chunkEvents), //调用_loadAsync
    chunkEvents: chunkEvents.stream,
    scale: key.scale,
    ... 
  );
}

MultiFrameImageStreamCompleter 是一个多帧图片管理器,是ImageStreamCompleter的一个子类。
MultiFrameImageStreamCompleter 需要一个Future<ui.Codec>类型的参数——codec。Codec 是处理图片编解码的类的一个handler,是一个flutter engine API 的包装类。图片的编解码的逻辑不是在Dart 代码部分实现,而是在flutter engine中实现的。

  MultiFrameImageStreamCompleter({
    @required Future<ui.Codec> codec,
    @required double scale,
    Stream<ImageChunkEvent> chunkEvents,
    InformationCollector informationCollector,
  }) : assert(codec != null),
       _informationCollector = informationCollector,
       _scale = scale {
    codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
      reportError(...);
    });
    if (chunkEvents != null) {
      chunkEvents.listen(
        (ImageChunkEvent event) {
          if (hasListeners) {
            // Make a copy to allow for concurrent modification.
            final List<ImageChunkListener> localListeners = _listeners
                .map<ImageChunkListener>((ImageStreamListener listener) => listener.onChunk)
                .where((ImageChunkListener chunkListener) => chunkListener != null)
                .toList();
            for (ImageChunkListener listener in localListeners) {
              listener(event);
            }
          }
        }, onError: (dynamic error, StackTrace stack) {//...},
      );
    }
  }

Codec类部分定义如下:

@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {
  // 此类由flutter engine创建,不应该手动实例化此类或直接继承此类。
  @pragma('vm:entry-point')
  Codec._();
  /// 图片中的帧数(动态图会有多帧)
  int get frameCount native 'Codec_frameCount';
  /// 动画重复的次数,0 -只执行一次,-1-循环执行
  int get repetitionCount native 'Codec_repetitionCount';
  /// 获取下一个动画帧
  Future<FrameInfo> getNextFrame() {
    return _futurize(_getNextFrame);
  }
  String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';
}

我们可以看到Codec最终的结果是一个或多个(动图)帧,而这些帧最终会绘制到屏幕上。
MultiFrameImageStreamCompleter 的 codec参数值为_loadAsync方法的返回值,我们继续看_loadAsync方法的实现:

Future<ui.Codec> _loadAsync(
    NetworkImage key,
    StreamController<ImageChunkEvent> chunkEvents,
  ) 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)
        throw Exception(...);
      // 接收图片数据 
      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);//PaintingBinding.instance.instantiateImageCodec(bytes)
    } finally {
      chunkEvents.close();
    }
  }

_loadAsync方法主要做了两件事:

下载逻辑比较简单:通过HttpClient从网上下载图片,另外下载请求会设置一些自定义的header,开发者可以通过NetworkImage的headers命名参数来传递。

在图片下载完成后调用了PaintingBinding.instance.instantiateImageCodec(bytes)对图片进行解码,instantiateImageCodec(...)也是一个Native API的包装,会调用Flutter engine的instantiateImageCodec方法,源码如下:

String _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)
  native 'instantiateImageCodec';

codec的异步方法执行完成后会调用_handleCodecReady函数。

//MultiFrameImageStreamCompleter
  void _handleCodecReady(ui.Codec codec) {
    _codec = codec;
    assert(_codec != null);

    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
  }

该方法将codec对象保存起来,然后解码图片帧

#MultiFrameImageStreamCompleter
  Future<void> _decodeNextFrameAndSchedule() async {
    try {
      _nextFrame = await _codec.getNextFrame();
    } catch (exception, stack) {
      reportError(...);
      return;
    }
    if (_codec.frameCount == 1) {
      // This is not an animated image, just return it and don't schedule more frames.
      _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
      return;
    }
    _scheduleAppFrame();
  }

如果只有一帧,则执行_emitFrame函数。从帧数据中拿到图片帧对象根据缩放比例创建ImageInfo对象,然后设置显示的图片信息

#MultiFrameImageStreamCompleter
  void _emitFrame(ImageInfo imageInfo) {
    setImage(imageInfo);
    _framesEmitted += 1;
  }

#ImageStreamCompleter
@protected
  void setImage(ImageInfo image) {
    _currentImage = image;
    if (_listeners.isEmpty)
      return;
    // Make a copy to allow for concurrent modification.
    final List<ImageStreamListener> localListeners =
        List<ImageStreamListener>.from(_listeners);
    for (ImageStreamListener listener in localListeners) {
      try {
        listener.onImage(image, false);
      } catch (exception, stack) {
        reportError(...);
      }
    }
  }

五、Image加载流程总结

整个流程大概如下:

六、如何减轻图片带来的内存压力?

//修改缓存最大值
const int _kDefaultSize = 100;
const int _kDefaultSizeBytes = 50 << 20;  

//退出页面清除缓存
  @override
  void dispose() {
    PaintingBinding.instance.imageCache.clear();
    super.dispose();
  }

七、添加磁盘缓存

上面我们已经知道,Image只有内存缓存,没有本地缓存。那么我们如何添加本地缓存呢?其实只需要改进NetWorkImage的_loadAsync方法。

Future<ui.Codec> _loadAsync(NetworkImage key,StreamController<ImageChunkEvent> chunkEvents, image_provider.DecoderCallback decode,) async {
    try {
      assert(key == this);
   //--------新增代码1 begin--------------
   // 判断是否有本地缓存
    final Uint8List cacheImageBytes = await ImageCacheUtil.getImageBytes(key.url);
    if(cacheImageBytes != null) {
      return decode(cacheImageBytes);
    }
   //--------新增代码1 end--------------

    //...省略
      if (bytes.lengthInBytes == 0)
        throw Exception('NetworkImage is an empty file: $resolved');

        //--------新增代码2 begin--------------
       // 缓存图片数据到本地,需要定制具体的缓存策略
       await ImageCacheUtil.saveImageBytesToLocal(key.url, bytes);
       //--------新增代码2 end--------------

      return decode(bytes);
    } finally {
      chunkEvents.close();
    }
  }
上一篇下一篇

猜你喜欢

热点阅读