开发者社区 > 博文 > Flutter之Image加载流程源码解读
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

Flutter之Image加载流程源码解读

  • 京麦研发团队
  • 2021-06-30
  • IP归属:未知
  • 65720浏览

Flutter呢,是一套UI框架,跨平台效果是唰唰的,近期得空看下Image图片加载的流程,顺手写下来大家一起瞅瞅,源码解读可能会累一点,有点心理准备哈。

首先呢,先说点基础,有基础的跳过吧,Flutter中一切皆组件,如图(flutter中文网里面截的0~ _~0),可见无状态组件与有状态组件为主,今天的主角Image是继承自StatefulWidget的。StatefulWidget的特点是有个State, State变化会触发组件的更新,一系列组件的更新就达成了用户界面的更新。基础完事~~~~

今天主要看的是Image图片从地址链接、ID等等最终加载到图片组件的流程。其他的控制图片大小、位置、展示类型什么的,我眼睛小顾不过来,索性就不要管了哈。

首先呢,Image的爸爸是StatefulWidget,那么下手点除了Image的构造方法外还得加个State。看下构造:

l   Image.network( String src,.... )

l   Image.file( File file, .... )

l   Image.asset( String name, ......  )

l   Image.memory( Uint8List bytes,....)

l   Image({Key key , @required this.image , this.frameBuilder, this.loadingBuilder,this.errorBuilder,....} )

除去全参数的以外,提供了日常使用场景的四个方法:网络、本地文件、内置资源、内存图片。也不少了够用,今天就单拎网络这个来看看,构造代码这样子的:

Image.network(
  String src, {
  Key key,
  double scale = 1.0,
  this.frameBuilder,
  this.loadingBuilder,
  this.errorBuilder,
  this.semanticLabel,
  this.excludeFromSemantics = false,
  this.width,
  this.height,
  this.color,
  this.colorBlendMode,
  this.fit,
  this.alignment = Alignment.center,
  this.repeat = ImageRepeat.noRepeat,
  this.centerSlice,
  this.matchTextDirection = false,
  this.gaplessPlayback = false,
  this.filterQuality = FilterQuality.low,
  Map<String, String> headers,
  int cacheWidth,
  int cacheHeight,
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
     assert(alignment != null),
     assert(repeat != null),
     assert(matchTextDirection != null),
     assert(cacheWidth == null || cacheWidth > 0),
     assert(cacheHeight == null || cacheHeight > 0),
     super(key: key);

入参中很多参数都是易识别的,如缓存宽、高,方向、是否重复等等,这些都是与图片的最终呈现样式相关的,不是今天的主要目的,暂时不管,剩下的一个主要的就是src图片地址了,很显然,从它入手顺腾摸瓜是一定可以找到今天的答案的。

构造方法中对src进行了处理:

image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers))

而源码中对image这个参数的注释是:

/// The image to display.
final ImageProvider image;

翻译:用于展示的image。一拍同桌大腿,没毛病,这家伙一定是个关键。 那就对ResizeImage下手吧,点进resizeIfNeeded方法,代码很少,单词基本也都认识,好险。

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;
}

这里是做了个缓存处理,不是我们关心的主逻辑,一边去,不关心缓存的话直接返回了入参NetworkImage。点进NetWorkImage源码:

/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
const factory NetworkImage(String url, { double scale, Map<String, String> headers }) = network_image.NetworkImage;

哈,是一常量,再点,看到类了…. _network_image_io.dart文件里的NetWrokImage,有点长下一步搞嘛? 鬼知道呢。。。。先看看类里都啥方法吧

load(…)、loadAsync(…)这俩方法应该是加载图片的关键方法呗,obtainKey这个方法。。。。嗯,生成一个key,后续怎么用干啥用的,没到阻断我们看加载流程的放一边去,估计是下载图片的时候的一个唯一标记吧,who care ~~~

容我睁大眼睛看下两个load方法,小眼一瞅,哎呦 load里面调用loadAsync方法,loadAsync方法只有load里面调用了,巧不巧,太省心了,源码,嘿,就这?!

Load方法:返回了个MultiFrameImageStreamCompleter的对象,loadAsync方法是他构造的入参,先不看Completer是个啥玩意,吃大瓜,捡大钱,嗯。。先看load方法吧:

@override
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
  // Ownership of this controller is handed off to [_loadAsync]; it is that
  // method's responsibility to close the controller's stream when the image
  // has been loaded or an error is thrown.
  final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

  return MultiFrameImageStreamCompleter(
    codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
    chunkEvents: chunkEvents.stream,
    scale: key.scale,
    informationCollector: () {
      return <DiagnosticsNode>[
        DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
        DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
      ];
    },
  );
}

这里返回了个:MultiFrameImageStreamCompleter对象,它构建入参有:codec编解码器chunkEvents事件块(没错我不认识chunk,查字典查的),其他。 codeC呢,是_loadAsync方法返回的,_loadAsync方法的入参有:key,chunkEvents,decode,decode应该是跟图片解码有关系的,算了看代码吧。。。猜个啥费脑细胞。。。先看下MultiFrameImageStreamCompleter

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(
      context: ErrorDescription('resolving an image codec'),
      exception: error,
      stack: stack,
      informationCollector: informationCollector,
      silent: true,
    );
  });
  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 (final ImageChunkListener listener in localListeners) {
            listener(event);
          }
        }
      }, onError: (dynamic error, StackTrace stack) {
        reportError(
          context: ErrorDescription('loading an image'),
          exception: error,
          stack: stack,
          informationCollector: informationCollector,
          silent: true,
        );
      },
    );
  }
}

构造方法里codeC对回调进行了处理,成功回调为_handleCodeReady方法,里面主要调用方法为:_decodeNextFrameAndSchedule()

Future<void> _decodeNextFrameAndSchedule() async {
  try {
    _nextFrame = await _codec.getNextFrame();
  } catch (exception, stack) {
    reportError(
      context: ErrorDescription('resolving an image frame'),
      exception: exception,
      stack: stack,
      informationCollector: _informationCollector,
      silent: true,
    );
    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方法体中主要方法为setImage(imageInfo),看下代码

/// Calls all the registered listeners to notify them of a new image.
@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 (final ImageStreamListener listener in localListeners) {
    try {
      listener.onImage(image, false);
    } catch (exception, stack) {
      reportError(
        context: ErrorDescription('by an image listener'),
        exception: exception,
        stack: stack,
      );
    }
  }
}

嗯,这里可以看到图片信息通过listener回传给了上层。也就是说后续我们需要关注completer的listenter谁给设置的。

回头继续看MultiFrameImageStreamCompleter的第一个入参codeC的生成方法_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) {
      // The network may be only temporarily unavailable, or the file will be
      // added on the server later. Avoid having future calls to resolve
      // fail to check the network again.
      PaintingBinding.instance.imageCache.evict(key);
      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();
  }
}

粗理下流程哈,进方法后先构建httprequest,叽里呱啦的,然后就发请求了,请求回来后先缓存逻辑先处理,然后构建Unit8List,然后解码方法对象decode处理,生成UI.CodeC对象。刹个车,看一眼这个Unit8list 看下构造,这里在收到回传字节时chunkEvents搞了件事,存下了下载进度。嗯 这玩意就是后续处理回调用的。 再看一眼CodeC.(嘘~~英文是编解码器的意思,查字典查到的)看类注释:

/// A handle to an image codec.
///
/// This class is created by the engine, and should not be instantiated
/// or extended directly.
///
/// To obtain an instance of the [Codec] interface, see
/// [instantiateImageCodec].
@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {

咳咳,翻译一下哈,英文不好凑活看:

“这是一个处理图片的编解码器

这个类呢 是系统创建出来的,你没啥事别自己去初始化或者去继承他。

要是想看看CodeC是咋创建的,去找instantiateImageCodec 这个类“

翻译完成,嗯,看了注释感觉这个类暂时不用细究。是吧哈,不过既然是编解码器,再多看一眼都啥方法吧,源码小白还是很好奇的:

class Codec extends NativeFieldWrapperClass2 {
  //
  // This class is created by the engine, and should not be instantiated
  // or extended directly.
  //
  // To obtain an instance of the [Codec] interface, see
  // [instantiateImageCodec].
  @pragma('vm:entry-point')
  Codec._();

  /// Number of frames in this image.
  int get frameCount native 'Codec_frameCount';

  /// Number of times to repeat the animation.
  ///
  /// * 0 when the animation should be played once.
  /// * -1 for infinity repetitions.
  int get repetitionCount native 'Codec_repetitionCount';

  /// Fetches the next animation frame.
  ///
  /// Wraps back to the first frame after returning the last frame.
  ///
  /// The returned future can complete with an error if the decoding has failed.
  Future<FrameInfo> getNextFrame() {
    return _futurize(_getNextFrame);
  }

  /// Returns an error message on failure, null on success.
  String _getNextFrame(_Callback<FrameInfo> callback) native 'Codec_getNextFrame';

  /// Release the resources used by this object. The object is no longer usable
  /// after this method is called.
  void dispose() native 'Codec_dispose';
}

Emmm~~~ 好了 这个就不多说了 看一眼得了。

回神了~~~回神了~~~

到这里,能跟进的就告一段落了。后续我们要找NetworkImage的load方法谁调用的触发的下载、listener谁塞进来的回调给上层,然后就串起来了。

Emmmm 从哪里入手呢。。。 哦 开篇说了 Image这个控件是StatefulWidget,看state吧,三个方法:build方法,可以知道Image组件生成的组件是个啥,或者说是谁处理绘制图片的。didChangeDependencies(),didUpdateWidget() 俩方法处理组件更新。先看组件更新吧,上代码:

@override
void didChangeDependencies() {
  _updateInvertColors();
  _resolveImage();

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

  super.didChangeDependencies();
}


@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();
}

两个方法大致看一眼,主要操作在_resolveImage 以及对listener的处理。先看resolveImage方法:

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

先看下方法的整体流程:先创建了一个provider 入参使用了咱们构造函数创建的provider,本次分析的是网络图片,前面咱们也分析了不考虑缓存的情况下,这个provider就是NetworkImage,接着调用provider的resolve方法生成了一个ImageStream,最后更新stream,SrcollAwareImageProvider方法可长了。。。所以先看外层的更新方法吧,过会儿再看SrcollAwareImageProvider的代码,看updateSourceStream(newStream)方法:

// Updates _imageStream to newStream, and moves the stream listener
// registration from the old stream to the new stream (if a listener was
// registered).
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());
}

可以看到流程是先防止重复操作,然后正在下载的移除监听(这里没做关闭下载操作哈,就是这个图片后台可能还在下载),接着重新赋值成员变量_imageStraem, 然后添加监听,先看是个啥监听吧

ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
  loadingBuilder ??= widget.loadingBuilder;
  _lastException = null;
  _lastStack = null;
  return ImageStreamListener(
    _handleImageFrame,
    onChunk: loadingBuilder == null ? null : _handleImageChunk,
    onError: widget.errorBuilder != null
      ? (dynamic error, StackTrace stackTrace) {
          setState(() {
            _lastException = error;
            _lastStack = stackTrace;
          });
        }
      : null,
  );
}

返回的对象ImageStreamListener三个入参,第一个处理图片信息,加载进度回调,错误回调,很清晰,看第一个入参吧:

void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
  setState(() {
    _imageInfo = imageInfo;
    _loadingProgress = null;
    _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1;
    _wasSynchronouslyLoaded |= synchronousCall;
  });
}

哦呵~全乎了,图片解析完后回调就是这里。再回到_imageStream.addListener(_getListener())方法:

void addListener(ImageStreamListener listener) {
  if (_completer != null)
    return _completer.addListener(listener);
  _listeners ??= <ImageStreamListener>[];
  _listeners.add(listener);
}

哦 这个逻辑是。。已关联了completer的话,就把新设置的给completer,没设置的话就先存起来,呵呵 难不成imageStream里面存储个listenerlist的对象就是为了暂时给completer存着用的,天哪,太伟大了。哦扯远了,前面咱们梳理MultiFrameImageStreamCompleter的listener是哪儿来的,基本明了了,state变化时Image生成imagestream,同时生成listener设置给ImageStream暂存,后续应该有个时机stream关联completer。得嘞,回调这里处理完了,回来继续看_resolveImage方法的SrcollAwareImageProvider的代码类介绍:

/// An [ImageProvider] that makes use of
/// [Scollable.recommendDeferredLoadingForContext] to avoid loading images when
/// rapidly scrolling.

简单翻一下:SrcollAwareImageProvider是一个用于使用Scollable.recommendDeferredLoadingForContext方法来避免快速滚动时加载图片的一个图片提供者。

简单分析下,这玩意就是针对存在快速滑动场景下加载图片的一个提供者。避免的主要类为Scollable.recommendDeferredLoadingForContext,。分析结果是,跟我们要找的答案没没毛线关系。给个面子看下里面都啥东西:

@optionalTypeArgs
class ScrollAwareImageProvider<T> extends ImageProvider<T> {
  /// Creates a [ScrollingAwareImageProvider].
  ///
  /// The [context] object is the [BuildContext] of the [State] using this
  /// provider. It is used to determine scrolling velocity during [resolve]. It
  /// must not be null.
  ///
  /// The [imageProvider] is used to create a key and load the image. It must
  /// not be null, and is assumed to interact with the cache in the normal way
  /// that [ImageProvider.resolveStreamForKey] does.
  const ScrollAwareImageProvider({
    @required this.context,
    @required this.imageProvider,
  }) : assert(context != null),
       assert(imageProvider != null);

  /// The context that may or may not be enclosed by a [Scrollable].
  ///
  /// Once [DisposableBuildContext.dispose] is called on this context,
  /// the provider will stop trying to resolve the image if it has not already
  /// been resolved.
  final DisposableBuildContext context;

  /// The wrapped image provider to delegate [obtainKey] and [load] to.
  final ImageProvider<T> imageProvider;

  @override
  void resolveStreamForKey(
    ImageConfiguration configuration,
    ImageStream stream,
    T key,
    ImageErrorListener handleError,
  ) {
    // Something managed to complete the stream, or it's already in the image
    // cache. Notify the wrapped provider and expect it to behave by not
    // reloading the image since it's already resolved.
    // Do this even if the context has gone out of the tree, since it will
    // update LRU information about the cache. Even though we never showed the
    // image, it was still touched more recently.
    // Do this before checking scrolling, so that if the bytes are available we
    // render them even though we're scrolling fast - there's no additional
    // allocations to do for texture memory, it's already there.
    if (stream.completer != null || PaintingBinding.instance.imageCache.containsKey(key)) {
      imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
      return;
    }
    // The context has gone out of the tree - ignore it.
    if (context.context == null) {
      return;
    }
    // Something still wants this image, but check if the context is scrolling
    // too fast before scheduling work that might never show on screen.
    // Try to get to end of the frame callbacks of the next frame, and then
    // check again.
    if (Scrollable.recommendDeferredLoadingForContext(context.context)) {
        SchedulerBinding.instance.scheduleFrameCallback((_) {
          scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError));
        });
        return;
    }
    // We are in the tree, we're not scrolling too fast, the cache doens't
    // have our image, and no one has otherwise completed the stream.  Go.
    imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
  }

  @override
  ImageStreamCompleter load(T key, DecoderCallback decode) => imageProvider.load(key, decode);

  @override
  Future<T> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration);
}

一共三个方法:内部逻辑都是调用传入的imageprovider的方法,嗯,这个看完了,回到第一步,resolveImage方法第一步构建了SrcollAwareImageProvider,里面干活的还是传入的provider(widget.image),第二步,provider调用resolve方法返回imageStream。看SrcollAwareImageProvider 它没这个方法,就找父类吧,还真有:

@nonVirtual
ImageStream resolve(ImageConfiguration configuration) {
  assert(configuration != null);
  final ImageStream stream = createStream(configuration);
  // Load the key (potentially asynchronously), set up an error handling zone,
  // and call resolveStreamForKey.
  _createErrorHandlerAndKey(
    configuration,
    (T key, ImageErrorListener errorHandler) {
      resolveStreamForKey(configuration, stream, key, errorHandler);
    },
    (T key, dynamic exception, StackTrace stack) async {
      await null; // wait an event turn in case a listener has been added to the image stream.
      final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
      stream.setCompleter(imageCompleter);
      InformationCollector collector;
      assert(() {
        collector = () sync* {
          yield DiagnosticsProperty<ImageProvider>('Image provider', this);
          yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
          yield DiagnosticsProperty<T>('Image key', key, defaultValue: null);
        };
        return true;
      }());
      imageCompleter.setError(
        exception: exception,
        stack: stack,
        context: ErrorDescription('while resolving an image'),
        silent: true, // could be a network error or whatnot
        informationCollector: collector
        );
      },
    );
  return stream;
}

老规矩,先看下大流程:1. CreateStream 生成了个stream,2.调用了个_createErrorHandlerAndKey方法搞了些事情。嗯 第二步代码有点长呀…不太想看- -哎。先看下CreateStream方法吧:

@protected
ImageStream createStream(ImageConfiguration configuration) {
  return ImageStream();
}

嘿,这代码我喜欢,看着是啥也没干,挺好的。好吧。。。直面第二步的长串代码吧。

/// This method is used by both [resolve] and [obtainCacheStatus] to ensure
/// that errors thrown during key creation are handled whether synchronous or
/// asynchronous.
void _createErrorHandlerAndKey(
  ImageConfiguration configuration,
  _KeyAndErrorHandlerCallback<T> successCallback,
  _AsyncKeyErrorHandler<T> errorCallback,
) {
  T obtainedKey;
  bool didError = false;
  Future<void> handleError(dynamic exception, StackTrace stack) async {
    if (didError) {
      return;
    }
    if (!didError) {
      errorCallback(obtainedKey, exception, stack);
    }
    didError = true;
  }

  // If an error is added to a synchronous completer before a listener has been
  // added, it can throw an error both into the zone and up the stack. Thus, it
  // looks like the error has been caught, but it is in fact also bubbling to the
  // zone. Since we cannot prevent all usage of Completer.sync here, or rather
  // that changing them would be too breaking, we instead hook into the same
  // zone mechanism to intercept the uncaught error and deliver it to the
  // image stream's error handler. Note that these errors may be duplicated,
  // hence the need for the `didError` flag.
  final Zone dangerZone = Zone.current.fork(
    specification: ZoneSpecification(
      handleUncaughtError: (Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) {
        handleError(error, stackTrace);
      }
    )
  );
  dangerZone.runGuarded(() {
    Future<T> key;
    try {
      key = obtainKey(configuration);
    } catch (error, stackTrace) {
      handleError(error, stackTrace);
      return;
    }
    key.then<void>((T key) {
      obtainedKey = key;
      try {
        successCallback(key, handleError);
      } catch (error, stackTrace) {
        handleError(error, stackTrace);
      }
    }).catchError(handleError);
  });
}

先看入参:图片配置信息,成功回调,错误回调。配置信息咱们这次不关心,错误暂时放一边,方法后续我们关注成功回调。然后方法流程:zone启动,根据传入配置,生成了key,然后执行成功、失败会回调。Key的生成:obtainKey方法,点击可以看到父类ImageProvider没有实现这个方法,因为这里是NetworkImage,所以看下NetworkImage的实现:

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

这里是创建了一个future,future知识这里不做展开,看下逻辑后续调用的then方法

class SynchronousFuture<T> implements Future<T> {
  /// Creates a synchronous future.
  ///
  /// See also:
  ///
  ///  * [new Future.value] for information about creating a regular
  ///    [Future] that completes with a value.
  SynchronousFuture(this._value);

  final T _value;

  @override
  Stream<T> asStream() {
    final StreamController<T> controller = StreamController<T>();
    controller.add(_value);
    controller.close();
    return controller.stream;
  }

  @override
  Future<T> catchError(Function onError, { bool test(Object error) }) => Completer<T>().future;

  @override
  Future<E> then<E>(FutureOr<E> f(T value), { Function onError }) {
    final dynamic result = f(_value);
    if (result is Future<E>)
      return result;
    return SynchronousFuture<E>(result as E);
  }

  @override
  Future<T> timeout(Duration timeLimit, { FutureOr<T> onTimeout() }) {
    return Future<T>.value(_value).timeout(timeLimit, onTimeout: onTimeout);
  }

  @override
  Future<T> whenComplete(FutureOr<dynamic> action()) {
    try {
      final FutureOr<dynamic> result = action();
      if (result is Future)
        return result.then<T>((dynamic value) => _value);
      return this;
    } catch (e, stack) {
      return Future<T>.error(e, stack);
    }
  }
}


可以看到入参是自己,then返回的也是自己,所以这里的key值是NetworkImage自己,之后咱们看下success后回调,成功后调用:resolveStreamForKey(configuration, stream, key, errorHandler);方法

@protected
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
  // This is an unusual edge case where someone has told us that they found
  // the image we want before getting to this method. We should avoid calling
  // load again, but still update the image cache with LRU information.
  if (stream.completer != null) {
    final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
      key,
      () => stream.completer,
      onError: handleError,
    );
    assert(identical(completer, stream.completer));
    return;
  }
  final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
    key,
    () => load(key, PaintingBinding.instance.instantiateImageCodec),
    onError: handleError,
  );
  if (completer != null) {
    stream.setCompleter(completer);
  }
}

哎哟,completer这里搞的找到了。啥也不说 先看下stream.setCompleter(completer)方法的都干了啥

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

如上所料,这个关联的时候将所有的streamlistener给了completer一份,好了,

继续咱们是第一次加载,stream里面的completer绝对null。。当然了啥时候不为空,课后作业了。。为空则调用PaintingBingding.instance.imageCache.putIfAbsent方法,看你下入参第二个熟悉不!?嘿嘿 咱们之前看到的load方法,load方法里进行了urlhttp请求,请求回来后第二个参数是CodeC编解码器,进行处理,跟前面的串起来了吧。换句话说就是zone触发了下载流程。继续看方法putIfAbsent,这方法是在ImageCache里面,太长先切一段看:

ImageStreamCompleter result = _pendingImages[key]?.completer;
// Nothing needs to be done because the image hasn't loaded yet.
if (result != null) {
  if (!kReleaseMode) {
    timelineTask.finish(arguments: <String, dynamic>{'result': 'pending'});
  }
  return result;
}
// Remove the provider from the list so that we can move it to the
// recently used position below.
// Don't use _touch here, which would trigger a check on cache size that is
// not needed since this is just moving an existing cache entry to the head.
final _CachedImage image = _cache.remove(key);
if (image != null) {
  if (!kReleaseMode) {
    timelineTask.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
  }
  // The image might have been keptAlive but had no listeners (so not live).
  // Make sure the cache starts tracking it as live again.
  _trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
  _cache[key] = image;
  return image.completer;
}

final _CachedImage liveImage = _liveImages[key];
if (liveImage != null) {
  _touch(key, liveImage, timelineTask);
  if (!kReleaseMode) {
    timelineTask.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
  }
  return liveImage.completer;
}


梳理下:

先从pendingImages(下载中)里面取,取到了返回走人,取不到,缓存找,找到后,重新标记活着(应该是处理标记防止回收)返回走人,没找到继续从活着但无引用的图片组里取,老规矩,取到带走。哇,已经三级缓存了算是,厉害。继续

try {
  result = loader();
  _trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
} catch (error, stackTrace) {
  if (!kReleaseMode) {
    timelineTask.finish(arguments: <String, dynamic>{
      'result': 'error',
      'error': error.toString(),
      'stackTrace': stackTrace.toString(),
    });
  }
  if (onError != null) {
    onError(error, stackTrace);
    return null;
  } else {
    rethrow;
  }
}
....
result.addListener(streamListener);

return result;

哦结束了,没更多缓存了。都没有取到,就用这个传入的completer了。回到来的地方

void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
  // This is an unusual edge case where someone has told us that they found
  // the image we want before getting to this method. We should avoid calling
  // load again, but still update the image cache with LRU information.
  if (stream.completer != null) {
    final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
      key,
      () => stream.completer,
      onError: handleError,
    );
    assert(identical(completer, stream.completer));
    return;
  }
  final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
    key,
    () => load(key, PaintingBinding.instance.instantiateImageCodec),
    onError: handleError,
  );
  if (completer != null) {
    stream.setCompleter(completer);
  }
}

嗯,stream 关联了completer。到这里,大流程就全部串完了。

简单总结一下:

State改变时:

1.     生成ImageStream,关联Image持有的listenerImageStream中持有的listener是为了后续给ImageCompleter使用,关联时如果有ImageCompleter,则listener直接给completer,没有后续imageStream关联ImageCompleter时,将自己所有的listenercompleter一份。

2.     Provider(这里的provider就是NetWorkImage)生成ImageStream时,会走ImageCacheputIfAbsent方法生成completer直接关联。

3.     ImageCache里面可以看到flutter对图片自带有三层内存缓存+一层源缓存(咱们这次主要看的是网络,同理其他fileasserts各自硬盘上也有数据,姑且也当做缓存吧)

,多想一点就是后续咱们如果考虑图片优化时,ImageCache这个类很关键。

4.     如果ImageCache中没有缓存时,会通过Provider(这里的provider就是NetWorkImage)的.load方法生成,生成的是MultiFrameImageStreamCompleter对象

5.     MultiFrameImageStreamCompleter的构造方法中我们可以看到CodeC编解码器(native层的逻辑)在编解码过程中回调给codeC,通过_handleCodecReady方法处理,handleCodecReady方法通过一层层逻辑最终调用自己的listenerImageStream给它的,ImageStream中的是Image控件设置进来的)回调给上层。

6.     结束~

扯。。。。 还差一点,谁用了最终ImageInfo里面的图片数据进行展示。是的,上面说过的Builde中可以看到是哪个控件。看代码:

@override
Widget build(BuildContext context) {
  if (_lastException  != null) {
    assert(widget.errorBuilder != null);
    return widget.errorBuilder(context, _lastException, _lastStack);
  }

  Widget result = RawImage(
    image: _imageInfo?.image,
    width: widget.width,
    height: widget.height,
    scale: _imageInfo?.scale ?? 1.0,
    color: widget.color,
    colorBlendMode: widget.colorBlendMode,
    fit: widget.fit,
    alignment: widget.alignment,
    repeat: widget.repeat,
    centerSlice: widget.centerSlice,
    matchTextDirection: widget.matchTextDirection,
    invertColors: _invertColors,
    filterQuality: widget.filterQuality,
  );

  if (!widget.excludeFromSemantics) {
    result = Semantics(
      container: widget.semanticLabel != null,
      image: true,
      label: widget.semanticLabel ?? '',
      child: result,
    );
  }

  if (widget.frameBuilder != null)
    result = widget.frameBuilder(context, result, _frameNumber, _wasSynchronouslyLoaded);

  if (widget.loadingBuilder != null)
    result = widget.loadingBuilder(context, result, _loadingProgress);

  return result;
}

Build方法里面返回的十个RawImage,第一个参数就是imageInfo,是的结束了,别多看一层吧,不是说有什么三棵树么,比别人多看深一点。点进去:

先看看注释:

Rawimage是直接展示ui.Image的控件,Image控件是通过paintImage方法绘制的,RawImage很少直接使用,用Image控件吧。

分析下:1. RawImage不要直接用,2.图片绘制使用的ui.image是通过paintImage方法处理的。

好了,那干脆利落的关注下paintImage方法吧,类里搜索下paintImage关键字。。。好漂亮,没搜到。。。咋整,看下RawImage的方法有个create有个update俩方法,看create吧,这里应该有线索,

@override
RenderImage createRenderObject(BuildContext context) {
  assert((!matchTextDirection && alignment is Alignment) || debugCheckHasDirectionality(context));
  return RenderImage(
    image: image,
    width: width,
    height: height,
    scale: scale,
    color: color,
    colorBlendMode: colorBlendMode,
    fit: fit,
    alignment: alignment,
    repeat: repeat,
    centerSlice: centerSlice,
    matchTextDirection: matchTextDirection,
    textDirection: matchTextDirection || alignment is! Alignment ? Directionality.of(context) : null,
    invertColors: invertColors,
    filterQuality: filterQuality,
  );
}

呵呵,创建了个renderImagerender我认识绘制的意思呗,它应该有用,点进去搜paintImage,找到了,

@override
void paint(PaintingContext context, Offset offset) {
  if (_image == null)
    return;
  _resolve();
  assert(_resolvedAlignment != null);
  assert(_flipHorizontally != null);
  paintImage(
    canvas: context.canvas,
    rect: offset & size,
    image: _image,
    scale: _scale,
    colorFilter: _colorFilter,
    fit: _fit,
    alignment: _resolvedAlignment,
    centerSlice: _centerSlice,
    repeat: _repeat,
    flipHorizontally: _flipHorizontally,
    invertColors: invertColors,
    filterQuality: _filterQuality,
  );
}

这里有画布canvas,应该基本看到曙光了,看方法(太长。。。截一段其他的自己翻下源码哈):

draw方法了。可以 就是它了。哎呀,完活,最后看一眼renderImage的类注释

/// An image in the render tree.
///
/// The render image attempts to find a size for itself that fits in the given
/// constraints and preserves the image's intrinsic aspect ratio.
///
/// The image is painted using [paintImage], which describes the meanings of the
/// various fields on this class in more detail.


renderImage是在render tree 树上的。 其他的不翻译了。词汇量太少。

 

Ok,至此,图片从一个地址到最终呈现在界面图片控件上的整个主流程就简单梳理完了,原谅我多蚊子,没给大家画整体的图,交给后人吧~~

注:

1.     作者是个Flutter小白,文章中有不对的,不严谨的,不适的地方大家多指正。哎,要是能贴个打赏码就更好了~~~

2.     不同的flutter版本源码不定一样,所以这篇帖子只是给大家理个思路(尽管里面可能我个人观点会掺杂),同时给准备看源码的小白看看,克服下源码恐惧感,所以大家尽量看完帖子自己再看梳理一遍吧。