Flutter图片加载自动缓存大小

我们在开发安卓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, cacheHeightResizeImage,我们查看源码,当设置了cacheWidthcacheHeight调用的是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;
}

我们测试设置cacheHeightcacheWidth的显示清晰度和过载情况,测试图片是一张高度大于宽度的图

注意这里的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)面积即可避免图片过载提示。

通常我们不能提前知道加载图片的宽高比,去设置cacheHeightcacheWidth来给图片设置一个合适的尺寸。

我们自定义一个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(),
        );
      })
相关推荐
萌面小侠Plus2 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
君蓦2 小时前
Flutter 本地存储与数据库的使用和优化
flutter
人工智能培训咨询叶梓6 小时前
探索开放资源上指令微调语言模型的现状
人工智能·语言模型·自然语言处理·性能优化·调优·大模型微调·指令微调
CodeToGym8 小时前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
无尽的大道9 小时前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
superman超哥9 小时前
04 深入 Oracle 并发世界:MVCC、锁、闩锁、事务隔离与并发性能优化的探索
数据库·oracle·性能优化·dba
problc11 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
前端青山19 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
lqj_本人20 小时前
鸿蒙next选择 Flutter 开发跨平台应用的原因
flutter·华为·harmonyos