• flutter的图片加载原理


    本文基于1.12.13+hotfix.8版本源码分析。

    1、Image

    点击进入源码,可以看到Image继承自StatefulWidget,那么重点自然在State里面。跟着生命周期走,可以发现在didUpdateWidget中调用了这个方法:

      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 (_isListeningToStream)
          _imageStream.addListener(_getListener());
      }
      
      ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
        loadingBuilder ??= widget.loadingBuilder;
        return ImageStreamListener(
          _handleImageFrame,
          onChunk: loadingBuilder == null ? null : _handleImageChunk,
        );
      }
    

    在这里调用了image(ImageProvider)的resolve方法获取到一个ImageStream,并给这个流设置了监听器。从名字上,不难猜出这是个图片数据流,在listener拿到数据后会调用setState(() {})方法进行rebuild,这里不再贴代码。

    2、ImageProvider

    在上面我们看到了Image是需要接收图片数据进行绘制的,那么,这个数据是在哪里解码的?又是哪里发送过来的?

    带着疑问,我们先进到ImageProvider的源码,可以发现其实这个类非常简单,代码量也不多,可以看看resolve方法的核心部分:

      Future<T> key;
      try {
        key = obtainKey(configuration);
      } catch (error, stackTrace) {
        handleError(error, stackTrace);
        return;
      }
      key.then<void>((T key) {
        obtainedKey = key;
        final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
          key,
          () => load(key, PaintingBinding.instance.instantiateImageCodec),
          onError: handleError,
        );
        if (completer != null) {
          stream.setCompleter(completer);
        }
      }).catchError(handleError);
    

    可以看到,这里会异步获取到一个key,然后从管理在PaintingBinding中的缓存池查找图片流。继续看关键的obtainKey和load方法,去到定义的地方,可以发现这两个都是子类实现的。从注释中可以看到,obtainKey的功能就是根据传入的ImageConfiguration生成一个独一无二的key(废话),而load方法则是将key转换成为一个ImageStreamCompleter对象并开始加载图片。

    那么,我们从最简单的MemoryImage入手,首先看看obtainKey:

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

    可以看到,就只是把自己包了一层再返回,并没有什么特殊。接着看load:

      @override
      ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) {
        return MultiFrameImageStreamCompleter(
          codec: _loadAsync(key, decode),
          scale: key.scale,
        );
      }
    
      Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) {
        assert(key == this);
        return decode(bytes);
      }
    

    同样非常简单,就是创建了一个ImageStreamCompleter的子类对象,同时传入了一个包装了解码器的Future(这个解码器是PaintingBinding.instance.instantiateImageCodec,内部调用native方法进行图片解码)。

    看到这里,相信基本有猜想了,数据和解码器都提供了,看来ImageStreamCompleter就是我们要看的数据源提供者。

    3、图片数据加载ImageStream、ImageStreamCompleter

    废话不多说,直接看MultiFrameImageStreamCompleter,可以看到直接在构造函数中获取codec对象,在获取到以后就会去获取解码数据,下面是简化的代码片段:

      // 构造函数中获取codec
      codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {// 略});
      
      void _handleCodecReady(ui.Codec codec) {
        _codec = codec;
        assert(_codec != null);
    
        if (hasListeners) {
          // 拿到codec之后解码数据
          _decodeNextFrameAndSchedule();
        }
      }
      
      Future<void> _decodeNextFrameAndSchedule() async {
        try {
          _nextFrame = await _codec.getNextFrame();
        } catch (exception, stack) {
          // 略
          return;
        }
        if (_codec.frameCount == 1) {
          // 发送数据
          _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
          return;
        }
        _scheduleAppFrame();
      }
    

    看到这里,终于找到了发送数据的地方,_emitFrame里面会调用setImage,而后在setImage中会找到listener并将数据发送,而listener就是widgets.Image注册的监听器。

    4、缓存池PaintingBinding#imageCache

    看完了加载流程,我们看看缓存池的缓存逻辑,回到ImageProvider的resolve方法,这里有个关键点,传给PaintingBinding的是个创建方法,而非实体。进入其源码可以看到是先检测cache中是否存在该对象,存在则直接返回,不存在才会调用load方法进行创建:

    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      // 有缓存就直接返回
      _cache[key] = image;
      return image.completer;
    }
    try {
      // 没找到缓存就调外面传入的loader()进行创建
      result = loader();
    } // catch部分省略
    

    并且,在刚创建时缓存中的对象是个PendingImage,这东西可以理解为类似一个占位符的作用,当图片数据加载完毕后才替换成实际数据对象CacheImage:

      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();
    }
    
    // 这里创建了一个PendingImage插入缓存
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      final ImageStreamListener streamListener = ImageStreamListener(listener);
      _pendingImages[key] = _PendingImage(result, streamListener);
      // 监听加载状态,result就是ImageStreamCompleter
      result.addListener(streamListener);
    }
    

    5、网络图片加载

    看完最基本的图片数据加载,接下来看看NetworkImage如何加载网络图片。看核心的load方法:

      ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
        final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
        return MultiFrameImageStreamCompleter(
          // 关键点1,加载、解析数据
          codec: _loadAsync(key, chunkEvents, decode),
          // 关键点2,分块下载事件流传给completer用
          chunkEvents: chunkEvents.stream,
          scale: key.scale,
        );
      }
    

    直接进入关键方法,看NetworkImage的_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)
            // 可以看到,图片下载失败是会抛异常的
            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);
        } finally {
          chunkEvents.close();
        }
      }
    

    这里有2个点:

    (1)通过HttpClient进行图片下载,下载失败或者数据为空都会抛异常,这里要做好异常处理。另外,从上面的图片缓存逻辑可以看到,flutter默认是只有内存缓存的,磁盘缓存需要自己处理,可以在这里入手处理;

    (2)通过consolidateHttpClientResponseBytes接收数据,并将下载进度转成ImageChunkEvent发送出去。可以看看MultiFrameImageStreamCompleter对ImageChunkEvent的处理:

    if (chunkEvents != null) {
      chunkEvents.listen(
        (ImageChunkEvent event) {
          if (hasListeners) {
            // 把这个事件传递给ImageStreamListener的onChunk方法
            final List<ImageChunkListener> localListeners = _listeners
                .map<ImageChunkListener>((ImageStreamListener listener) => listener.onChunk)
                .where((ImageChunkListener chunkListener) => chunkListener != null)
                .toList();
            for (ImageChunkListener listener in localListeners) {
              listener(event);
            }
          }
        }
      );
    }
    

    顺着_listeners的来源,一路往上找,最后可以看到onChunk方法是这里传进来的:

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

    widget.loadingBuilder即自定义loading状态的方法。

  • 相关阅读:
    【20220511】起风了
    【力扣 083】154. 寻找旋转排序数组中的最小值 II
    【力扣 082】153. 寻找旋转排序数组中的最小值
    【力扣 084】240. 搜索二维矩阵 II
    【力扣 081】658. 找到 K 个最接近的元素
    xshell和xftp下载免费版本方法
    端到端的特征转换示例:使用三元组损失和 CNN 进行特征提取和转换
    GAN能进行股票预测吗?
    LSTM 又回来了! 新论文使用LSTM挑战长序列建模的 ViT
    3 个不常见但非常实用的Pandas 使用技巧
  • 原文地址:https://www.cnblogs.com/joahyau/p/12637797.html
Copyright © 2020-2023  润新知