刚上手Flutter时会碰到几个图片加载的插件,刚开始可能无所谓性能好坏,能满足我们的需求就是好的插件 ;当我们遇到图片加载慢、加载图片失败时,我们就会去分析到底哪个图片插件是最适合我们,或者说为了满足我们的需求,我们要自己写一个图片插件。不多说,就常用的几个图片加载插件去分析一下看看到底哪个适合你。
FadeInImage
//assetNetwork 用于加载网络图片
FadeInImage.assetNetwork(placeholder: '', image: '')
//调用后会生成一个ImageProvider 类型的 image;
image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),
可以看出FadeInImage.assetNetwork() 通过NetworkImage() 去加载图片;但是这个ResizeImage类是干嘛用的?assetNetwork又起了什么作用呢?稍后再去解释这个类,我们先走完主要流程。(文章顺序是按照代码流程走,先介绍图像绘制
后介绍图片网络加载
)
// build会用到这个方法,先了解一下
Image _image({
// 这个参数就是刚才生成的image
@required ImageProvider image,
//图片加载错误的errorBuilder
ImageErrorWidgetBuilder errorBuilder,
//加载第一帧时的效果(如loadingBuild,淡入淡出效果的Build)
ImageFrameBuilder frameBuilder,
})
return Image(
image: image,
errorBuilder: errorBuilder,
frameBuilder: frameBuilder,
...
);
}
//build 才是重点
@override
Widget build(BuildContext context) {
//通过_image 方法将 ImageProvider 类型的 image 转换成 Widget
Widget result = _image(
image: image,
errorBuilder: imageErrorBuilder,
frameBuilder: (BuildContext context, Widget child, int frame, bool wasSynchronouslyLoaded) {
//是否同步加载,默认是异步的,不会走这个
if (wasSynchronouslyLoaded)
return child;
//_AnimatedFadeOutFadeIn()方法是渐入渐出动画效果
return _AnimatedFadeOutFadeIn(
target: child,
placeholder: _image(image: placeholder, errorBuilder: placeholderErrorBuilder),
isTargetLoaded: frame != null,
fadeInDuration: fadeInDuration,
fadeOutDuration: fadeOutDuration,
fadeInCurve: fadeInCurve,
fadeOutCurve: fadeOutCurve,
);
},
);
//excludeFromSemantics 默认是false,肯定对走下面的Semantics()方法,这个Semantics不做过多解释,是个辅助工具
if (!excludeFromSemantics) {
result = Semantics(
container: imageSemanticLabel != null,
image: true,
label: imageSemanticLabel ?? '',
child: result,
);
}
return result;
}
Semantics(语义) 用于描述Widget的含义最终达到描述应用程序的UI。这些描述可以通过辅助工具、搜索引擎和其他语义分析软件使用。它有点像HTML5的语义元素,在Android、iOS上更多是用于读屏,帮助一些有视力障碍的人使用我们的软件(Android TalkBack 和 iOS VoiceOver)。
1.图像绘制
走到这里我们并没有看到图片的网络请求是在哪里调用的?图片是如何绘制的?只是看到最终返回一个result
,so 问题肯定出在result
身上了,继续深扒_image()
方法里的Image
,看它是怎么作妖的。
class Image extends StatefulWidget {
//...省略部分代码
@override
Widget build(BuildContext context) {
Widget result = RawImage(
image: _imageInfo?.image,
width: widget.width,
height: widget.height,
scale: _imageInfo?.scale ?? 1.0
filterQuality: widget.filterQuality,//默认低质量
//...
);
//图片没有加载完成会执行frameBuilder 或者loadingBuilder, 会多次刷新这个build
if (widget.frameBuilder != null)
result = widget.frameBuilder(context, result, _frameNumber, _wasSynchronouslyLoaded);
if (widget.loadingBuilder != null)
result = widget.loadingBuilder(context, result, _loadingProgress);
return result;
}
}
class RawImage extends LeafRenderObjectWidget {
///The image is painted using [paintImage],paintImage绘制图片
/// Creates a widget that displays an image.创建一个widget显示图片
const RawImage({
Key key,
this.image,
this.width,
this.height,
this.scale = 1.0,
this.color,
this.colorBlendMode,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection = false,
this.invertColors = false,
this.filterQuality = FilterQuality.low,
})
//这里有个createRenderObject方法
RenderImage createRenderObject(BuildContext context) {
assert((!matchTextDirection && alignment is Alignment) || debugCheckHasDirectionality(context));
return RenderImage(
image: image,
//...
);
}
//继续看这个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.
///渲染图像试图为自己找到一个适合给定约束的大小,并保持图像固有的高宽比。
class RenderImage extends RenderBox {
//这个类就是绘制图像了
@override
void paint(PaintingContext context, Offset offset) {
///...绘制方法
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,
);
///...
}
}
到此这是图片绘制过程,但是这个绘制的Image 到底是啥?因为一开始参数传递时是ImageProvider,最终怎么会变成了Image 类型呢?推测这个应该是个流,但绘制时直接传了Image,有点懵了,可能是将流封装成了Image吧。继续深扒
void paintImage({
@required Canvas canvas,
@required Rect rect,
@required ui.Image image,
double scale = 1.0,
...
}) {
//...
canvas.drawImageRect(image, sourceRect, destinationRect, paint);
//...
}
void drawImageRect(Image image, Rect src, Rect dst, Paint paint) {
}
//还是传了一个Image 类型
class Image extends NativeFieldWrapperClass2 {
// ...
//原来是封装成了Image了
//Converts the [Image] object into a byte array.将image对象封装成流
Future toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}) {
return _futurize((_Callback callback) {
return _toByteData(format.index, (Uint8List encoded) {
callback(encoded?.buffer?.asByteData());
});
});
}
绘制有了,就差网络请求了,只要能请求到数据流,然后封装成Image对象,就可以绘制图片了,让我们继续看它是如何从网络获取数据的。
2.网络加载
image = ResizeImage.resizeIfNeeded(imageCacheWidth, imageCacheHeight, NetworkImage(image, scale: imageScale)),
上面提到这个方法 NetworkImage(),图片的获取就是从这个方法开始的,看看这个方法到底怎么实现的。
1.可以看到这个NetworkImage是个抽象类,继承了ImageProvider
abstract class NetworkImage extends ImageProvider {
/// 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 headers }) = network_image.NetworkImage;
//...省略部分代码
}
2.所有的图片类都继承了这个ImageProvider,这个类至关重要,弄明白这个类就能懂个90%了
ImageProvider是个抽象类,定义了图片数据获取和加载的相关接口。它的主要职责有两个:
(1) 提供图片数据源
(2) 缓存图片
abstract class ImageProvider {
ImageStream resolve(ImageConfiguration configuration) {
// 实现代码省略
}
Future evict({ ImageCache cache,
ImageConfiguration configuration = ImageConfiguration.empty }) async {
// 实现代码省略
}
//缓存key
Future obtainKey(ImageConfiguration configuration);
@protected
ImageStreamCompleter load(T key); // 需子类实现,加载load
}
1.load(T key)方法
加载图片数据源的接口,不同的数据源的加载方法不同,每个ImageProvider
的子类必须实现它。比如NetworkImage
类和AssetImage
类,它们都是ImageProvider
的子类,但它们需要从不同的数据源来加载图片数据:NetworkImage
是从网络来加载图片数据,而AssetImage
则是从最终的应用包里来加载(加载打到应用安装包里的资源图片)。 我们以NetworkImage
为例,看看其load方法的实现:
// _network_image_io.dart
@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 chunkEvents = StreamController();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
informationCollector: () {
return [
DiagnosticsProperty('Image provider', this),
DiagnosticsProperty('Image key', key),
];
},
);
}
我们看到,load方法的返回值类型是ImageStreamCompleter
,它是一个抽象类,定义了管理图片加载过程的一些接口,Image
Widget中正是通过它来监听图片加载状态的。
MultiFrameImageStreamCompleter
是 ImageStreamCompleter
的一个子类,是flutter sdk
预置的类,通过该类,我们以方便、轻松地创建出一个ImageStreamCompleter
实例来做为load方法的返回值。
我们可以看到,MultiFrameImageStreamCompleter
需要一个codec
参数,该参数类型为Future
。Codec
是处理图片编解码的类的一个handler
,实际上,它只是一个flutter engine API
的包装类,也就是说图片的编解码逻辑不是在Dart 代码部分实现,而是在flutter engine
中实现的。Codec
类部分定义如下:
class Codec extends NativeFieldWrapperClass2 {
// 此类由flutter engine创建,不应该手动实例化此类或直接继承此类。
// This class is created by the engine, and should not be instantiated
// or extended directly.
//
//注意看这个里,_()需要看instantiateImageCodec()方法,等会会用到
// To obtain an instance of the [Codec] interface, see
// [instantiateImageCodec].
@pragma('vm:entry-point')
Codec._();
/// 图片中的帧数(动态图会有多帧)
int get frameCount native 'Codec_frameCount';
/// 动画重复的次数
/// * 0 表示只执行一次
/// * -1 表示循环执行
int get repetitionCount native 'Codec_repetitionCount';
/// 获取下一个动画帧
Future getNextFrame() {
return _futurize(_getNextFrame);
}
/// Returns an error message on failure, null on success.
String _getNextFrame(_Callback callback) native 'Codec_getNextFrame';
}
我们可以看到Codec
最终的结果是一个或多个(动图)帧,而这些帧最终会绘制到屏幕上。
MultiFrameImageStreamCompleter
的 codec
参数值为_loadAsync
方法的返回值,我们继续看_loadAsync
方法的实现:
Future _loadAsync(
NetworkImage key,
StreamController chunkEvents,
image_provider.DecoderCallback decode,
) async {
try {
//下载图片
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) {
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();
}
}
可以看到_loadAsync
方法主要做了两件事:
1.下载图片
2.对下载的图片数据进行解码
下载逻辑比较简单:通过HttpClient
从网上下载图片,另外下载请求会设置一些自定义的header
,开发者可以通过NetworkImage
的headers
命名参数来传递。
在图片下载完成后调用了decode(bytes)
对数据进行回调;
typedef DecoderCallback = Future Function(Uint8List bytes, {int cacheWidth, int cacheHeight});
会封装成一个ui.Codec
类型;
// To obtain an instance of the [Codec] interface, see
// [instantiateImageCodec].
Codec._();
//_()需要看instantiateImageCodec();方法
Future instantiateImageCodec(Uint8List list, {
int targetWidth,
int targetHeight,
}) {
return _futurize(
(_Callback callback) => _instantiateImageCodec(list, callback, null, targetWidth ?? _kDoNotResizeDimension, targetHeight ?? _kDoNotResizeDimension)
);
}
//最终会调用一个native方法
String _instantiateImageCodec(Uint8List list, _Callback callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)
native 'instantiateImageCodec';
通过多个回调最终调用Flutter engine
的instantiateImageCodec
的方法去解析图片。
obtainKey(ImageConfiguration configuration)方法
@override
Future obtainKey(image_provider.ImageConfiguration configuration) {
//this指代NetworkImage对象
return SynchronousFuture(this);
}
//缓存的key是根据 url 和 scale
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is NetworkImage
&& other.url == url
&& other.scale == scale;
}
key
是根据图片的url
和scale
生成的
该接口主要是为了配合实现图片缓存,ImageProvider
从数据源加载完数据后,会在全局的ImageCache
中缓存图片数据,而图片数据缓存是一个Map
,而Map
的key
便是调用此方法的返回值,不同的key
代表不同的图片数据缓存。
resolve(ImageConfiguration configuration)方法
resolve
方法是ImageProvider
的暴露的给·Image·的主入口方法,它接受一个ImageConfiguration
参数,返回ImageStream
,即图片数据流。我们重点看一下resolve
执行流程:
@nonVirtual
ImageStream resolve(ImageConfiguration configuration) {
assert(configuration != null);
final ImageStream stream = createStream(configuration);
_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;
//...省略部分代码
},
);
return stream;
}
void _createErrorHandlerAndKey(
ImageConfiguration configuration,
_KeyAndErrorHandlerCallback successCallback,///缓存会在这里执行
_AsyncKeyErrorHandler errorCallback,
) {
/// 创建一个新Zone,主要是为了当发生错误时不会干扰MainZone
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 key;
try {
// 生成缓存key,后面会根据此key来检测是否有缓存
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then((T key) {
obtainedKey = key;
try {
//成功回调
successCallback(key, handleError);
} catch (error, stackTrace) {
handleError(error, stackTrace);
}
}).catchError(handleError);
});
}
ImageConfiguration
包含图片和设备的相关信息,如图片的大小、所在的AssetBundle
(只有打到安装包的图片存在)以及当前的设备平台、devicePixelRatio
(设备像素比等)。Flutter SDK
提供了一个便捷函数createLocalImageConfiguration
来创建ImageConfiguration
对象:
通过ResizeImage
名字可以联想到这是调整图片大小,看官方给出的解释
class ImageConfiguration {
const ImageConfiguration({
this.bundle,
this.devicePixelRatio,
this.locale,
this.textDirection,
this.size,
this.platform,
});
}
上面提到的缓存现在来看看:
@protected
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
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);
}
}
PaintingBinding.instance.imageCache 是 ImageCache的一个实例,它是PaintingBinding的一个属性,而Flutter框架中的PaintingBinding.instance是一个单例,imageCache事实上也是一个单例,也就是说图片缓存是全局的,统一由PaintingBinding.instance.imageCache 来管理。
篇幅太长还没写完......