Flutter加载图片流程之ResizeImage、NetworkImage源码解析(四)

1,196 阅读6分钟

ResizeImage

/// Instructs Flutter to decode the image at the specified dimensions
/// instead of at its native size.
///
/// This allows finer control of the size of the image in [ImageCache] and is
/// generally used to reduce the memory footprint of [ImageCache].
///
/// The decoded image may still be displayed at sizes other than the
/// cached size provided here.
class ResizeImage extends ImageProvider<ResizeImageKey> {}

ResizeImage 是一个用于在指定尺寸下对图像进行解码的类,而不是以其原始尺寸进行解码。它继承自 ImageProvider 类,用于对图像进行缓存和显示。使用该类可以更细粒度地控制图像在 ImageCache 中的尺寸,通常用于减小 ImageCache 的内存占用。需要注意的是,解码后的图像仍然可以在其他尺寸下显示。

ResizeImage初始化、width、height

/// Creates an ImageProvider that decodes the image to the specified size.
///
/// The cached image will be directly decoded and stored at the resolution
/// defined by `width` and `height`. The image will lose detail and
/// use less memory if resized to a size smaller than the native size.
const ResizeImage(
 this.imageProvider, {
 this.width,
 this.height,
 this.allowUpscaling = false,
}) : assert(width != null || height != null),
    assert(allowUpscaling != null);

/// The [ImageProvider] that this class wraps.
final ImageProvider imageProvider;

/// The width the image should decode to and cache.
final int? width;

/// The height the image should decode to and cache.
final int? height;

/// Whether the [width] and [height] parameters should be clamped to the
/// intrinsic width and height of the image.
///
/// In general, it is better for memory usage to avoid scaling the image
/// beyond its intrinsic dimensions when decoding it. If there is a need to
/// scale an image larger, it is better to apply a scale to the canvas, or
/// to use an appropriate [Image.fit].
final bool allowUpscaling;

loadBuffer

@override
ImageStreamCompleter loadBuffer(ResizeImageKey key, DecoderBufferCallback decode) {
  Future<ui.Codec> decodeResize(ui.ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
    assert(
      cacheWidth == null && cacheHeight == null && allowUpscaling == null,
      'ResizeImage cannot be composed with another ImageProvider that applies '
      'cacheWidth, cacheHeight, or allowUpscaling.',
    );
    return decode(buffer, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling);
  }
  final ImageStreamCompleter completer = imageProvider.loadBuffer(key._providerCacheKey, decodeResize);
  if (!kReleaseMode) {
    completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})';
  }
  return completer;
}

真正decode图片数据成指定width、height。

PaintingBinding

instantiateImageCodec

Future<ui.Codec> instantiateImageCodec(
  Uint8List bytes, {
  int? cacheWidth,
  int? cacheHeight,
  bool allowUpscaling = false,
}) {
  assert(cacheWidth == null || cacheWidth > 0);
  assert(cacheHeight == null || cacheHeight > 0);
  assert(allowUpscaling != null);
  return ui.instantiateImageCodec(
    bytes,
    targetWidth: cacheWidth,
    targetHeight: cacheHeight,
    allowUpscaling: allowUpscaling,
  );
}

instantiateImageCodecFromBuffer

Future<Codec> instantiateImageCodecFromBuffer(
  ImmutableBuffer buffer, {
  int? targetWidth,
  int? targetHeight,
  bool allowUpscaling = true,
}) async {
  final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer);
  if (!allowUpscaling) {
    if (targetWidth != null && targetWidth > descriptor.width) {
      targetWidth = descriptor.width;
    }
    if (targetHeight != null && targetHeight > descriptor.height) {
      targetHeight = descriptor.height;
    }
  }
  buffer.dispose();
  return descriptor.instantiateCodec(
    targetWidth: targetWidth,
    targetHeight: targetHeight,
  );
}

instantiateCodec

Future<Codec> instantiateCodec({int? targetWidth, int? targetHeight}) async {
  if (targetWidth != null && targetWidth <= 0) {
    targetWidth = null;
  }
  if (targetHeight != null && targetHeight <= 0) {
    targetHeight = null;
  }

  if (targetWidth == null && targetHeight == null) {
    targetWidth = width;
    targetHeight = height;
  } else if (targetWidth == null && targetHeight != null) {
    targetWidth = (targetHeight * (width / height)).round();
    targetHeight = targetHeight;
  } else if (targetHeight == null && targetWidth != null) {
    targetWidth = targetWidth;
    targetHeight = targetWidth ~/ (width / height);
  }
  assert(targetWidth != null);
  assert(targetHeight != null);

  final Codec codec = Codec._();
  _instantiateCodec(codec, targetWidth!, targetHeight!);
  return codec;
}

_instantiateCodec

void _instantiateCodec(Codec outCodec, int targetWidth, int targetHeight) native 'ImageDescriptor_instantiateCodec';

abstract NetworkImage

abstract class NetworkImage extends ImageProvider<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;

  /// The URL from which the image will be fetched.
  String get url;

  /// The scale to place in the [ImageInfo] object of the image.
  double get scale;

  /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
  ///
  /// When running flutter on the web, headers are not used.
  Map<String, String>? get headers;

  @override
  ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);

  @override
  ImageStreamCompleter loadBuffer(NetworkImage key, DecoderBufferCallback decode);
}

从官方注释,我们可知:

  • NetworkImage是一个用于从网络获取图像并显示的Flutter小部件的背后的图像提供程序。它将给定的URL与给定的比例相关联,并通过创建一个可在UI树中使用的ImageStreamCompleter对象来处理图像。

  • 该类在内部实现了一个名为MultiFrameImageStreamCompleter的多帧图像流完成器,以便支持GIF图像和逐帧动画。

  • 如果是Web平台上的网络图像,那么[DecoderCallback]的cacheWidthcacheHeight参数将被忽略,因为Web引擎将网络图像的图像解码委托给Web,而Web不支持自定义解码大小。

  • 在未来版本中,官方希望能够找到一种方法来遵循缓存标头,以便在最后一次引用图像时,如果标头将该图像描述为已过期,则我们可以预先从缓存中清除图像。

NetworkImage提供了工厂方法 const factory NetworkImage(String url, { double scale, Map<String, String>? headers }) = network_image.NetworkImage;。该方法关联到NetworkImage的内部实现。下面我们一起探讨下。

implements NetworkImage

/// Creates an object that fetches the image at the given URL.
///
/// The arguments [url] and [scale] must not be null.
const NetworkImage(this.url, { this.scale = 1.0, this.headers })
  : assert(url != null),
    assert(scale != null);

@override
final String url;

@override
final double scale;

@override
final Map<String, String>? headers;

NetworkImage是一个用于从网络上获取图像的ImageProvider,它需要一个url参数来指定图像的URL,并且还可以指定scale参数来指定缩放比例。headers参数用于指定可选的HTTP请求头,这可以用于验证、身份验证或其他目的。它是一个不可变的类,创建后不能更改。assert关键字用于确保参数不为空。

下面,实现的是重要的方法。

obtainKey

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

这是NetworkImage类的方法,用于获取一个标识该图像的键。在这个方法中,它直接返回一个SynchronousFuture对象,表示该图像已经准备好并且可以被显示。因为网络图像的URL已经是其唯一标识符,所以这里直接返回this作为图像的键。

load (被废弃。暂不讨论)

loadBuffer

ImageStreamCompleter loadBuffer(image_provider.NetworkImage key, image_provider.DecoderBufferCallback 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, null),
    chunkEvents: chunkEvents.stream,
    scale: key.scale,
    debugLabel: key.url,
    informationCollector: () => <DiagnosticsNode>[
      DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
      DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
    ],
  );
}

该方法用于从网络加载图像数据,并生成一个对应的 ImageStreamCompleter 对象。其中,key 是一个 NetworkImage 对象,包含了要加载的图像的 URL 地址和缩放比例等信息。decode 是一个 DecoderBufferCallback 对象,表示如何解码获取到的二进制数据。返回的 ImageStreamCompleter 对象可以用于在 Image widget 中显示网络图像。

_httpClient

// Do not access this field directly; use [_httpClient] instead.
// We set `autoUncompress` to false to ensure that we can trust the value of
// the `Content-Length` HTTP header. We automatically uncompress the content
// in our call to [consolidateHttpClientResponseBytes].
static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;

static HttpClient get _httpClient {
  HttpClient client = _sharedHttpClient;
  assert(() {
    if (debugNetworkImageHttpClientProvider != null) {
      client = debugNetworkImageHttpClientProvider!();
    }
    return true;
  }());
  return client;
}

这段代码实现了一个静态的 _sharedHttpClient 变量,它是 HttpClient 类型的对象,并设置了 autoUncompress 属性为 false。这个变量用来发送 HTTP 请求,它在整个应用程序中只会被创建一次。

另外,还实现了一个静态的 _httpClient getter 方法,用于获取 _sharedHttpClient 对象。如果存在一个 debugNetworkImageHttpClientProvider 函数,则会调用该函数,以便在调试时使用定制的 HttpClient 对象。

总之,这个类提供了一个静态的 _sharedHttpClient 对象和 _httpClient getter 方法,用于发送 HTTP 请求,并可以通过调试函数来提供定制的 HttpClient 对象。这有助于管理应用程序中的网络请求,以及方便进行调试。

_loadAsync(重要方法。发送http请求,下载网络数据)

Future<ui.Codec> _loadAsync(
  NetworkImage key,
  StreamController<ImageChunkEvent> chunkEvents,
  image_provider.DecoderBufferCallback? decode,
  image_provider.DecoderCallback? decodeDepreacted,
) 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.
      await response.drain<List<int>>(<int>[]);
      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');
    }

    if (decode != null) {
      final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
      return decode(buffer);
    } else {
      assert(decodeDepreacted != null);
      return decodeDepreacted!(bytes);
    }
  } catch (e) {
    // Depending on where the exception was thrown, the image cache may not
    // have had a chance to track the key in the cache at all.
    // Schedule a microtask to give the cache a chance to add the key.
    scheduleMicrotask(() {
      PaintingBinding.instance.imageCache.evict(key);
    });
    rethrow;
  } finally {
    chunkEvents.close();
  }
}

从源码可知。这才是真正异步加载网络图片的地方。

MultiFrameImageStreamCompleter 主要通过 codec 参数获得渲染数据,而这个数据来源通过 _loadAsync 方法得到,该方法主要通过 http 下载图片后,对图片数据通过 PaintingBinding 进行 ImageCodec 编码处理,将图片转化为引擎可绘制数据。

这是一个异步方法,用于在网络上获取给定URL的图像。它使用HttpClient来发出网络请求,并返回一个Future,其中包含用于解码图像数据的Codec。

参数key是一个NetworkImage对象,它封装了要加载的图像的URL和其他配置参数。参数chunkEvents是一个StreamController,用于传输加载过程中接收到的ImageChunkEvent事件,以便在加载过程中可以显示进度条。

  1. 首先定义了一个异步方法 _loadAsync,该方法接收四个参数:
  • key,一个 NetworkImage 对象,表示要加载的图片;
  • chunkEvents,一个 StreamController<ImageChunkEvent> 对象,用于向外部通知加载过程中的图片数据块事件;
  • decode,一个可选的 DecoderBufferCallback 回调函数,用于对加载完成的图片字节数组进行解码;
  • decodeDepreacted,一个可选的 DecoderCallback 回调函数,用于对加载完成的图片字节数组进行解码(已经废弃不再使用)。
swiftCopy code
  Future<ui.Codec> _loadAsync(
    NetworkImage key,
    StreamController<ImageChunkEvent> chunkEvents,
    image_provider.DecoderBufferCallback? decode,
    image_provider.DecoderCallback? decodeDepreacted,
  ) async {
  1. 然后,将传入的 key 转换成 Uri 对象,表示图片的下载地址。
javaCopy code
      final Uri resolved = Uri.base.resolve(key.url);
  1. 接下来,创建一个 HttpClientRequest 对象,用于发起网络请求,并设置请求头(如果有的话)。
rustCopy code
      final HttpClientRequest request = await _httpClient.getUrl(resolved);

      headers?.forEach((String name, String value) {
        request.headers.add(name, value);
      });
  1. 发送网络请求并获取响应,如果响应状态码不是 200,表示请求失败,抛出 NetworkImageLoadException 异常。
phpCopy code
      final HttpClientResponse response = await request.close();
      if (response.statusCode != HttpStatus.ok) {
        await response.drain<List<int>>(<int>[]);
        throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
      }
  1. 下载完成后,将响应数据合并为一个 Uint8List,并调用回调函数进行解码,返回一个 ui.Codec 对象。如果没有传入 decode 参数,则使用 decodeDepreacted 进行解码。如果字节数组为空,则抛出异常。
javaCopy code
      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');
      }

      if (decode != null) {
        final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
        return decode(buffer);
      } else {
        assert(decodeDepreacted != null);
        return decodeDepreacted!(bytes);
      }
  1. 最后,在 try...catch...finally 代码块中,将 chunkEvents 关闭,并根据是否抛出异常来清除图片缓存。
scssCopy code
    } catch (e) {
      scheduleMicrotask(() {
        PaintingBinding.instance.imageCache.evict(key);
      });
      rethrow;
    } finally {
      chunkEvents

辅助方法

@override
bool operator ==(Object other) {
  if (other.runtimeType != runtimeType) {
    return false;
  }
  return other is NetworkImage
      && other.url == url
      && other.scale == scale;
}

@override
int get hashCode => Object.hash(url, scale);

@override
String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)';

consolidateHttpClientResponseBytes

/// Signature for getting notified when chunks of bytes are received while
/// consolidating the bytes of an [HttpClientResponse] into a [Uint8List].
///
/// The `cumulative` parameter will contain the total number of bytes received
/// thus far. If the response has been gzipped, this number will be the number
/// of compressed bytes that have been received _across the wire_.
///
/// The `total` parameter will contain the _expected_ total number of bytes to
/// be received across the wire (extracted from the value of the
/// `Content-Length` HTTP response header), or null if the size of the response
/// body is not known in advance (this is common for HTTP chunked transfer
/// encoding, which itself is common when a large amount of data is being
/// returned to the client and the total size of the response may not be known
/// until the request has been fully processed).
///
/// This is used in [consolidateHttpClientResponseBytes].
typedef BytesReceivedCallback = void Function(int cumulative, int? total);

/// Efficiently converts the response body of an [HttpClientResponse] into a
/// [Uint8List].
///
/// The future returned will forward any error emitted by `response`.
///
/// The `onBytesReceived` callback, if specified, will be invoked for every
/// chunk of bytes that is received while consolidating the response bytes.
/// If the callback throws an error, processing of the response will halt, and
/// the returned future will complete with the error that was thrown by the
/// callback. For more information on how to interpret the parameters to the
/// callback, see the documentation on [BytesReceivedCallback].
///
/// If the `response` is gzipped and the `autoUncompress` parameter is true,
/// this will automatically un-compress the bytes in the returned list if it
/// hasn't already been done via [HttpClient.autoUncompress]. To get compressed
/// bytes from this method (assuming the response is sending compressed bytes),
/// set both [HttpClient.autoUncompress] to false and the `autoUncompress`
/// parameter to false.
Future<Uint8List> consolidateHttpClientResponseBytes(
  HttpClientResponse response, {
  bool autoUncompress = true,
  BytesReceivedCallback? onBytesReceived,
}) {
  assert(autoUncompress != null);
  final Completer<Uint8List> completer = Completer<Uint8List>.sync();

  final _OutputBuffer output = _OutputBuffer();
  ByteConversionSink sink = output;
  int? expectedContentLength = response.contentLength;
  if (expectedContentLength == -1) {
    expectedContentLength = null;
  }
  switch (response.compressionState) {
    case HttpClientResponseCompressionState.compressed:
      if (autoUncompress) {
        // We need to un-compress the bytes as they come in.
        sink = gzip.decoder.startChunkedConversion(output);
      }
      break;
    case HttpClientResponseCompressionState.decompressed:
      // response.contentLength will not match our bytes stream, so we declare
      // that we don't know the expected content length.
      expectedContentLength = null;
      break;
    case HttpClientResponseCompressionState.notCompressed:
      // Fall-through.
      break;
  }

  int bytesReceived = 0;
  late final StreamSubscription<List<int>> subscription;
  subscription = response.listen((List<int> chunk) {
    sink.add(chunk);
    if (onBytesReceived != null) {
      bytesReceived += chunk.length;
      try {
        onBytesReceived(bytesReceived, expectedContentLength);
      } catch (error, stackTrace) {
        completer.completeError(error, stackTrace);
        subscription.cancel();
        return;
      }
    }
  }, onDone: () {
    sink.close();
    completer.complete(output.bytes);
  }, onError: completer.completeError, cancelOnError: true);

  return completer.future;
}

将响应数据合并为一个 Uint8List

总结

NetworkImage 是 Flutter 中的一种图片加载方式,用于从网络加载图片。它需要一个 URL 字符串参数来指定图片的位置。NetworkImage 在加载图片时会使用 HttpClient 发起 HTTP 请求,然后使用 ImageProvider 提供的 DecoderCallbackDecoderBufferCallback 对图片进行解码,最终将解码后的 ui.Codec 包装成 ImageStreamCompleter 返回。

NetworkImage 提供了 scaleheaders 两个可选参数,分别用于指定图片的缩放比例和 HTTP 请求头。scale 默认值为 1.0,表示不缩放;headers 默认值为 null,表示不添加 HTTP 请求头。

NetworkImage 内部,它通过 _httpClient 来获取 HttpClient 实例,而 _sharedHttpClientNetworkImage 内部使用的全局 HttpClient 实例,它的 autoUncompress 属性被设置为 false,以便确保可以信任 Content-Length HTTP 标头的值。_httpClient 中的 assert 调用会检查 debugNetworkImageHttpClientProvider 是否为非空值,如果是,则使用该函数提供的 HttpClient

_loadAsync 方法是 NetworkImage 内部用于异步加载图片的核心方法。它使用 _httpClient 发起 HTTP 请求,获取图片的二进制数据,并将其解码成 ui.Codec 对象。在这个过程中,它会将获取二进制数据的进度通过 chunkEvents 的流传递给调用者。如果请求失败,它会抛出 NetworkImageLoadException 异常,并调用 evict 方法从 ImageCache 中删除该图片。

参考链接

深入图片加载流程

Flutter图片缓存 | Image.network源码分析