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(),
        );
      })
相关推荐
AiFlutter2 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
苏三说技术3 小时前
Redis 性能优化的18招
数据库·redis·性能优化
程序猿会指北5 小时前
【鸿蒙(HarmonyOS)性能优化指南】内存分析器Allocation Profiler
性能优化·移动开发·harmonyos·openharmony·arkui·组件化·鸿蒙开发
程序猿会指北8 小时前
【鸿蒙(HarmonyOS)性能优化指南】启动分析工具Launch Profiler
c++·性能优化·harmonyos·openharmony·arkui·启动优化·鸿蒙开发
彭亚川Allen21 小时前
优化了2年的性能,没想到最后被数据库连接池坑了一把
数据库·后端·性能优化
MClink1 天前
Go怎么做性能优化工具篇之pprof
开发语言·性能优化·golang
京东零售技术1 天前
Taro小程序开发性能优化实践
性能优化·taro
理想不理想v1 天前
wepack如何进行性能优化
性能优化
m0_748247801 天前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
迷雾漫步者1 天前
Flutter组件————PageView
flutter·跨平台·dart