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状态的方法。

posted @ 2020-04-05 16:36  jyau  阅读(1902)  评论(0编辑  收藏  举报