flutter源码学习笔记-图片加载流程

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

0、大纲

  1. Image
  2. ImageProvider
  3. 图片数据加载 ImageStream、ImageStreamCompleter
  4. 缓存池 PaintingBinding#imageCache
  5. 网络图片加载

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