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(),
        );
      })
相关推荐
孤影过客10 分钟前
Flutter优雅构建:从零打造开发级工作流
arm开发·数据库·flutter
IDC02_FEIYA34 分钟前
SQL Server 2016及SQL Server Management Studio下载,SQL Server 2016数据库安装教程图解
服务器·数据库·性能优化
小羊子说36 分钟前
Android 车机开发中常用的adb 脚本(更新中)
android·linux·adb·性能优化·车载系统
p1gd0g2 小时前
flutter web 如何确保用户收到更新
flutter
GoCoding2 小时前
Flutter ngspice 插件
flutter
恋猫de小郭2 小时前
Android Studio Panda 2 ,支持 AI 用 Vibe Coding 创建项目
android·前端·flutter
莫爷2 小时前
JSON 进阶技巧:Schema 验证、JSONPath 查询、性能优化
性能优化·json
Gorit4 小时前
如何使用 Flutter 开发 HarmonyOS 应用
flutter·华为·harmonyos
孤影过客4 小时前
Flutter高性能任务管理APP开发实战代码解析
jvm·flutter·oracle
键盘鼓手苏苏16 小时前
Flutter 三方库 p2plib 的鸿蒙化适配指南 - 实现高性能的端到端(P2P)加密通讯、支持分布式节点发现与去中心化数据流传输实战
flutter·harmonyos·鸿蒙·openharmony