美文网首页Flutter
Flutter系列之Image加载原理

Flutter系列之Image加载原理

作者: XQSY | 来源:发表于2020-09-16 19:38 被阅读0次

    一、前言

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

    二、Image

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


    image.png

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

    • Image用来显示图片。
    • _ImageState处理生命周期,生成Widget。
    • ImageProvider用来加载图片,生成key。
    • NetWorkImage是具体执行下载的,将下载的图片转化成ui.Codec,然后由ImageStreamCompleter去处理。
    • ImageStreamCompleter用来逐帧解析图片。
    • ImageStream是存储加载结果监听器List的。
    • MultiFrameImageStreamCompleter是多帧图片解析器。
    • ImageStreamListener 实际监听加载结果

    下面我们开始看下源码。

    构造函数
    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。

    • src:图片的url
    • image:必选参数,一个ImageProvide对象,图片的提供者,在调用的时候已经实例化,稍后会具体介绍。
    //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;
      ···
    }
    
    • _imageStream
      处理Image Resource的,ImageStream里存储着图片加载完毕的监听回调
    • _imageInfo
      Image的数据源信息:width和height以及ui.Image。 将ImageInfo里的ui.Image设置给RawImage就可以展示了。RawImage就是我们真正渲染的对象
    生命周期函数
    • initState
     @override
      void initState() {
        super.initState();
        WidgetsBinding.instance.addObserver(this);//监听生命周期
      }
    
    • didChangeDependencies
     @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 添加监听,等待图片数据

    • didUpdateWidget
     @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();
      }
    
    • build
     @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); 
    }
    
    • resolve
      获取数据流
    • evict
      清除缓存
    • obtainKey
      配合实现图片缓存
    • load
      加载图片数据源

    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
    • ImageCache
    • obtainKey 方法
    • ImageStreamCompleter
    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。
    这里提醒大家两个地方:

    • 图片缓存是在内存中,没有进行本地存储。
    • 应用生命周期内,如果缓存没有超过上限,相同的图片(key相同)只会被下载一次。
    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加载流程总结

    整个流程大概如下:

    • Image构造函数先实例化一个ImageProvider
    • 在_ImageState的didChangeDependencies方法中通过ImageProvider的resolve方法创建ImageStream对象,并关联一个ImageStreamCompleter,之后添加用于监听加载流程的ImageStreamListener1。
    • 在获取ImageStreamCompleter的过程中,如果有缓存,就从缓存中获取ImageStreamCompleter,如果没有缓存,就调用ImageProvider的load方法去加载图片并返回ImageStreamCompleter对象,然后给ImageStreamCompleter添加ImageStreamListener2。
    • load方法执行中会通过 http 下载图片,再经过PaintingBinding 编码转化后,得到ui.Codec可绘制对象,然后MultiFrameImageStreamCompleter调用_handleCodecReady方法把ui.Codec封装成ImageInfo。
    • 接着MultiFrameImageStreamCompleter会调用setImage方法,此方法触发加载监听ImageStreamListener1和ImageStreamListener2。
    • ImageStreamListener1回调到_ImageState,将ImageInfo保存, ImageStreamListener2的回调会把Image缓存下来。
    • _ImageState的 build方法中的会根据ImageInfo构建一个 RawImage 对象。
    • 最后 RawImage中的 RenderImage 通过paint方法绘制Widget。

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

    //修改缓存最大值
    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();
        }
      }
    

    相关文章

      网友评论

        本文标题:Flutter系列之Image加载原理

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