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]的
cacheWidth和cacheHeight参数将被忽略,因为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事件,以便在加载过程中可以显示进度条。
- 首先定义了一个异步方法
_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 {
- 然后,将传入的
key转换成Uri对象,表示图片的下载地址。
javaCopy code
final Uri resolved = Uri.base.resolve(key.url);
- 接下来,创建一个
HttpClientRequest对象,用于发起网络请求,并设置请求头(如果有的话)。
rustCopy code
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
- 发送网络请求并获取响应,如果响应状态码不是 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);
}
- 下载完成后,将响应数据合并为一个
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);
}
- 最后,在
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 提供的 DecoderCallback 或 DecoderBufferCallback 对图片进行解码,最终将解码后的 ui.Codec 包装成 ImageStreamCompleter 返回。
NetworkImage 提供了 scale 和 headers 两个可选参数,分别用于指定图片的缩放比例和 HTTP 请求头。scale 默认值为 1.0,表示不缩放;headers 默认值为 null,表示不添加 HTTP 请求头。
在 NetworkImage 内部,它通过 _httpClient 来获取 HttpClient 实例,而 _sharedHttpClient 是 NetworkImage 内部使用的全局 HttpClient 实例,它的 autoUncompress 属性被设置为 false,以便确保可以信任 Content-Length HTTP 标头的值。_httpClient 中的 assert 调用会检查 debugNetworkImageHttpClientProvider 是否为非空值,如果是,则使用该函数提供的 HttpClient。
_loadAsync 方法是 NetworkImage 内部用于异步加载图片的核心方法。它使用 _httpClient 发起 HTTP 请求,获取图片的二进制数据,并将其解码成 ui.Codec 对象。在这个过程中,它会将获取二进制数据的进度通过 chunkEvents 的流传递给调用者。如果请求失败,它会抛出 NetworkImageLoadException 异常,并调用 evict 方法从 ImageCache 中删除该图片。