我们在开发安卓APP时,使用的Glide去加载图片,他会默认按照view的尺寸进行图片缓存。但是在Flutter中,默认缓存使用的是图片大小。这会导致图片加载过程中占用内存比较大。如果你在在iOS设备上,加载一个图片列表,每张图片在2M左右时,应用会崩溃。
接下来我们通过自定义AutoResizeImage来解决这个问题
功能
支持各类 ImageProvider:包括NetworkImage
,AssetImage
,FileImage
等,支持CachedNetworkImageProvider
自动根据widget尺寸计算缓存图片大小,防止图片过载,加快加载速度
参数设置ResizeMode
可以设置不同的模式
参数设置scale
支持调整清晰度
现象
在Flutter的开发过程中,我们很熟悉的使用各种类型的图片加载。当你加载一张尺寸比较大(9248x6944)的图片时,你会发现图片加载的很慢,即使它是本地图片。
arduino
Image.asset(
Assets.imgBig,
width: 200,
height: 200,
),
这时打开debugInvertOversizedImages = true;
这个配置,可以看到图片会颜色反转同时倒置,同时日志会提示图片过载。
less
======== Exception caught by painting library ======================================================
The following message was thrown while painting an image:
Image assets/img/big.jpg has a display size of 525×525 but a decode size of 6151×8192, which uses an additional 261007KB.
Consider resizing the asset ahead of time, supplying a cacheWidth parameter of 525, a cacheHeight parameter of 525, or using a ResizeImage.
====================================================================================================
提示我们使用cacheWidth
, cacheHeight
或ResizeImage
,我们查看源码,当设置了cacheWidth
或cacheHeight
调用的是ResizeImage
的方法
javascript
static ImageProvider<Object> resizeIfNeeded(int? cacheWidth, int? cacheHeight, ImageProvider<Object> provider) {
if (cacheWidth != null || cacheHeight != null) {
return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
}
return provider;
}
我们测试设置cacheHeight
和cacheWidth
的显示清晰度和过载情况,测试图片是一张高度大于宽度的图
注意这里的525 = 200 * PaintingBinding.instance.window.devicePixelRatio
,是widget在设备上显示的像素值
ini
debugInvertOversizedImages = true;
Image.asset(
Assets.imgBig,
width: 200,
height: 200,
cacheHeight: 525,
),
单独使用cacheHeight | 单独使用cacheWidth | 同时使用cacheHeight和cacheWidth | |
---|---|---|---|
清晰度 | 低 | 高 | 图片拉伸变形 |
过载 | 否 | 是 | 否 |
原理
通过搜索debugInvertOversizedImages
发现控制过载显示的信息在源码decoration_image.dart
中,省略部分代码
dart
void paintImage({
···
}) {
···
if (!kReleaseMode) {
···
assert(() {
if (debugInvertOversizedImages &&
sizeInfo.decodedSizeInBytes > sizeInfo.displaySizeInBytes + debugImageOverheadAllowance) {
final int overheadInKilobytes = (sizeInfo.decodedSizeInBytes - sizeInfo.displaySizeInBytes) ~/ 1024;
final int outputWidth = sizeInfo.displaySize.width.toInt();
final int outputHeight = sizeInfo.displaySize.height.toInt();
FlutterError.reportError(FlutterErrorDetails(
exception: 'Image $debugImageLabel has a display size of '
'$outputWidth×$outputHeight but a decode size of '
'${image.width}×${image.height}, which uses an additional '
'${overheadInKilobytes}KB.\n\n'
'Consider resizing the asset ahead of time, supplying a cacheWidth '
'parameter of $outputWidth, a cacheHeight parameter of '
'$outputHeight, or using a ResizeImage.',
library: 'painting library',
context: ErrorDescription('while painting an image'),
));
// Invert the colors of the canvas.
canvas.saveLayer(
destinationRect,
Paint()..colorFilter = const ColorFilter.matrix(<double>[
-1, 0, 0, 0, 255,
0, -1, 0, 0, 255,
0, 0, -1, 0, 255,
0, 0, 0, 1, 0,
]),
);
// Flip the canvas vertically.
final double dy = -(rect.top + rect.height / 2.0);
canvas.translate(0.0, -dy);
canvas.scale(1.0, -1.0);
canvas.translate(0.0, dy);
invertedCanvas = true;
}
return true;
}());
···
}
可以看到只要满足sizeInfo.decodedSizeInBytes > sizeInfo.displaySizeInBytes + debugImageOverheadAllowance
会显示过载提示
arduino
/// The number of bytes needed to render the image without scaling it.
int get displaySizeInBytes => _sizeToBytes(displaySize);
/// The number of bytes used by the image in memory.
int get decodedSizeInBytes => _sizeToBytes(imageSize);
int _sizeToBytes(Size size) {
// Assume 4 bytes per pixel and that mipmapping will be used, which adds
// 4/3.
return (size.width * size.height * 4 * (4/3)).toInt();
}
解决
通过上面代码可以说明只要widget(displaySize)的面积小于图片(imageSize)面积即可避免图片过载提示。
通常我们不能提前知道加载图片的宽高比,去设置cacheHeight
或cacheWidth
来给图片设置一个合适的尺寸。
我们自定义一个AutoResizeImage去替换系统的ResizeImage
,自动根据widget的尺寸计算图片的尺寸,主要修改loadBuffer
。
ini
@override
ImageStreamCompleter loadBuffer(AutoResizeImageKey key, DecoderBufferCallback decode) {
Future<Codec> decodeResize(ImmutableBuffer buffer, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) async {
assert(
cacheWidth == null && cacheHeight == null && allowUpscaling == null,
'ResizeImage cannot be composed with another ImageProvider that applies '
'cacheWidth, cacheHeight, or allowUpscaling.',
);
final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer);
Size resize = _resize(descriptor);
return descriptor.instantiateCodec(
targetWidth: resize.width.round(),
targetHeight: resize.height.round(),
);
}
final ImageStreamCompleter completer = imageProvider.loadBuffer(key._providerCacheKey, decodeResize);
if (!kReleaseMode) {
completer.debugLabel = '${completer.debugLabel} - Resized(${key._width}×${key._height})';
}
return completer;
}
Size _resize(ImageDescriptor descriptor) {
var displayWidth = width * PaintingBinding.instance.window.devicePixelRatio;
var displayHeight = height * PaintingBinding.instance.window.devicePixelRatio;
var displayAspectRatio = displayWidth / displayHeight;
int imageWidth = descriptor.width;
int imageHeight = descriptor.height;
double imageAspectRatio = imageWidth / imageHeight;
double targetWidth;
double targetHeight;
if (imageWidth * imageHeight <= displayWidth * displayHeight) {
targetWidth = imageWidth.toDouble();
targetHeight = imageHeight.toDouble();
} else {
//need resize
var mode = imageAspectRatio / displayAspectRatio > overRatio || (1 / imageAspectRatio) / (1 / displayAspectRatio) > overRatio
? ResizeMode.cover
: resizeMode;
switch (mode) {
case ResizeMode.contain:
if (imageAspectRatio > 1) {
//wide
targetWidth = displayWidth;
targetHeight = displayWidth / imageAspectRatio;
} else {
//long
targetWidth = displayHeight * imageAspectRatio;
targetHeight = displayHeight;
}
break;
case ResizeMode.cover:
if (imageAspectRatio > 1) {
//wide
targetWidth = displayHeight * imageAspectRatio;
targetHeight = displayHeight;
} else {
//long
targetWidth = displayWidth;
targetHeight = displayWidth / imageAspectRatio;
}
break;
case ResizeMode.balance:
double scale = sqrt((displayWidth * displayHeight) / (imageWidth * imageHeight));
targetWidth = imageWidth * scale;
targetHeight = imageHeight * scale;
break;
}
}
return Size(targetWidth * scale, targetHeight * scale);
}
定义ResizeMode满足不同情况,绿色框为控件尺寸,红色框为图片缓存尺寸
ResizeMode | 图示 | 清晰度/内存占用 | Oversized |
---|---|---|---|
contain | 低 | 否 | |
balance | 中 | 否 | |
cover | 高 | 是 |
使用
基本使用
看这里example
debugInvertOversizedImages = false | debugInvertOversizedImages = true |
---|---|
CachedNetworkImage占位
CachedNetworkImage
使用OctoImage
实现占位,这里我们做一下调整
less
LayoutBuilder(builder: (context, constraints) {
return OctoImage(
image: AutoResizeImage(
imageProvider: CachedNetworkImageProvider(url),
width: constraints.maxWidth,
height: constraints.maxHeight,
),
placeholderBuilder: (_) => _buildPlaceHolder(),
errorBuilder: (_, __, ___) => _buildError(),
);
})