likes
comments
collection
share

Flutter图片加载优化探索

作者站长头像
站长
· 阅读数 36

​ 在Flutter的日常开发中,Image是非常常见的一个控件。无论是加载资源图,还是网络图,都离不开此控件。如果使用的不好,在多大图的列表里,内存占用可能会出现非常恐怖,来个直观图的了解一下:

Flutter图片加载优化探索

​ 当列表中图片过多的时候,内存占用很轻松的飙升到了六七百MB,这是一个很夸张的数值,如果机器的配置不够,很可能就会因此而崩溃了。可见,图片加载的优化是非常重要的。我在做图片加载优化的版本是stable 1.22.6,当时优化完的效果大概如下图:

Flutter图片加载优化探索

​ 可以看到优化的效果还是很明显的。不过后续我的项目的Flutter版本升级到了stable 2.0.x版本,我发现在新版本中官方对Image控件和ImageCache都做了一定的优化,所以目前来说我很推荐大家能够把项目的Flutter版本升级上去。

​ 想要进行优化首先还是得了解一下基本原理。

Image加载流程

Image本身是一个StatefulWidget里,widget本身都是一些配置,状态相关的交互都在_ImageState中。Image自身为我们提供了数个构造,我们可以很方便的加载不同来源的图片。看了构造方法后我们就会知道,不管是那种构造方法,都离不开成员ImageProviderImageProvider的作用把不同来源的图片加载到内存中。

/// The image to display.
final ImageProvider image;

​ 下面开始分析一个图片是如何被加载和展示的。

_ImageState.didChangeDependencies

Image的加载逻辑始于didChangeDependencies方法。

[->flutter/lib/src/widgets/image.dart]

void didChangeDependencies() {
  _updateInvertColors();
  _resolveImage();//处理ImageProvider

  if (TickerMode.of(context))//ticker是否开启,默认为true
    _listenToStream();//监听流
  else
    _stopListeningToStream(keepStreamAlive: true);

  super.didChangeDependencies();
}

_ImageState._resolveImage

[->flutter/lib/src/widgets/image.dart]

void _resolveImage() {
  //防止快速滑动加载的wrapper 包裹Widget里创建的ImageProvider
  final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
    context: _scrollAwareContext,
    imageProvider: widget.image,
  );
  final ImageStream newStream =
    //创建ImageStream
    provider.resolve(createLocalImageConfiguration(
      context,
      size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
    ));
  assert(newStream != null);
  //更新流
  _updateSourceStream(newStream);
}

ImageProvider.resolve

创建流并为ImageStream流并设置ImageStreamCompleter回调。

[->flutter/lib/src/painting/image_provider.dart]

ImageStream resolve(ImageConfiguration configuration) {
  assert(configuration != null);
  final ImageStream stream = createStream(configuration);
  // Load the key (potentially asynchronously), set up an error handling zone,
  // and call resolveStreamForKey.
  _createErrorHandlerAndKey(
    configuration,
    (T key, ImageErrorListener errorHandler) {
      //尝试为stream设置ImageStreamCompleter
      resolveStreamForKey(configuration, stream, key, errorHandler);
    },
    (T? key, Object exception, StackTrace? stack) async {
      ///...
    },
  );
  return stream;
}

ImageProvider.resolveStreamForKey

尝试为创建的ImageStream设置一个ImageSreamCompleter实例

[->flutter/lib/src/painting/image_provider.dart]

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,
    //此closure会调用ImageProvider.load方法
    //此处注意load方法的第二个参数为PaintingBinding.instance!.instantiateImageCodec
    () => load(key, PaintingBinding.instance!.instantiateImageCodec),
    onError: handleError,
  );
  if (completer != null) {
    stream.setCompleter(completer);
  }
}

ImageCache.putIfAbsent

尝试将请求放入全局缓存ImageCache并设置监听

[->flutter/lib/src/painting/image_cache.dart]

ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener? onError }) {
  ImageStreamCompleter? result = _pendingImages[key]?.completer;
  //如果是第一次加载,此处为null
  if (result != null) {
    return result;
  }
  final _CachedImage? image = _cache.remove(key);
    //如果是第一次加载,此处为null
  if (image != null) {
    //保证此ImageStream存活,存入活跃map里
    _trackLiveImage(
      key,
      image.completer,
      image.sizeBytes,
    );
    //缓存此Image
    _cache[key] = image;
    return image.completer;
  }
  final _LiveImage? liveImage = _liveImages[key];
   //如果是第一次加载,此处为null
  if (liveImage != null) {
    //此_LiveImage的流可能已经完成,具体条件为sizeBytes不为空
    //如果未完成,则会释放_CachedImage创建的aliveHandler
    _touch(
      key,
      _CachedImage(
        liveImage.completer,
        sizeBytes: liveImage.sizeBytes,
      ),
      timelineTask,
    );
    return liveImage.completer;
  }

  try {
    result = loader();//如果缓存未命中,会调用ImageProvider.load方法
    _trackLiveImage(key, result, null);//保证流不被dispose
  } catch (error, stackTrace) {
  }
  bool listenedOnce = false;
  _PendingImage? untrackedPendingImage;
  void listener(ImageInfo? info, bool syncCall) {
    int? sizeBytes;
    if (info != null) {
      sizeBytes = info.image.height * info.image.width * 4;
      //每一个Listener都会造成ImageInfo.image引用计数+1,如果不释放会造成image无法被释放。释放对此_Image的处理
      info.dispose();
    }
    //活跃计数+1
    final _CachedImage image = _CachedImage(
      result!,
      sizeBytes: sizeBytes,
    );
     //活跃计数+1 也可能无视
    _trackLiveImage(key, result, sizeBytes);
    if (untrackedPendingImage == null) {
      //允许缓存,则缓存_CachedImage
      _touch(key, image, listenerTask);
    } else {
      //直接释放图片
      image.dispose();
    }
    final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
    if (pendingImage != null) {
      //移除加载中的图片的监听,此时如果是最后一个,则_LiveImage也会被释放
      pendingImage.removeListener();
    }
    listenedOnce = true;
  }

  final ImageStreamListener streamListener = ImageStreamListener(listener);
  if (maximumSize > 0 && maximumSizeBytes > 0) {
    //存入加载中的map
    _pendingImages[key] = _PendingImage(result, streamListener);
  } else {
    //未设置缓存也会用一个field保存 防止前面存入_LiveImage导致的内存泄漏
    untrackedPendingImage = _PendingImage(result, streamListener);
  }
  // 走到这里,为ImageProvider.load方法返回的compeleter 注册监听.
  result.addListener(streamListener);//如果ImageStreamCompleter._currentImage不为空,会立刻回调
  return result;
}

ImageProvider.load

[->flutter/lib/src/painting/_network_image_io.dart]

ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
  //创建异步加载的事件流控制器
  final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
	//创建实际的ImageCompleter实现类
  return MultiFrameImageStreamCompleter(
    //图片解码回调
    codec: _loadAsync(key as NetworkImage, chunkEvents, decode),//异步加载方法
    //异步加载的流
    chunkEvents: chunkEvents.stream,
    scale: key.scale,
    debugLabel: key.url,
    informationCollector: () {
      return <DiagnosticsNode>[
        DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
        DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
      ];
    },
  );
}

ImageProvider的此方法是抽象方法,以NetworkProvider为例,这里会创建异步加载的时间流控制器,并创建实际的ImageStreamCompleter实现类 MultiFrameImageStreamCompleterImageStreamCompleter的实现类还有一个OneFrameImageStreamCompleter,不过目前官方的源码里还有使用的地方。

NetworkProvider._loadAsync

[->flutter/lib/src/painting/_network_image_io.dart]

Future<ui.Codec> _loadAsync(
  NetworkImage key,
  StreamController<ImageChunkEvent> chunkEvents,
  image_provider.DecoderCallback decode,
) async {
  try {
    assert(key == this);

    final Uri resolved = Uri.base.resolve(key.url);
		//使用HttpClient发起网络请求
    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) {
      throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
    }
		//Response转换成字节数组
    final Uint8List bytes = await consolidateHttpClientResponseBytes(
      response,
      onBytesReceived: (int cumulative, int? total) {
        //发送数据流Event
        chunkEvents.add(ImageChunkEvent(
          cumulativeBytesLoaded: cumulative,
          expectedTotalBytes: total,
        ));
      },
    );
    if (bytes.lengthInBytes == 0)
      throw Exception('NetworkImage is an empty file: $resolved');
		//使用DecoderCallback处理原始数据
    return decode(bytes);
  } catch (e) {
    scheduleMicrotask(() {
      PaintingBinding.instance!.imageCache!.evict(key);
    });
    rethrow;
  } finally {
    chunkEvents.close();
  }
}

​ 此方法是实际加载图片源数据的方法,不同的数据源会有不同的逻辑。本质都是获取到图片的原始字节数据,然后通过DecoderCallback来处理原始数据返回。DecoderCallback一般情况为PaintingBinding.instance!.instantiateImageCodec

_ImageState._updateSourceStream

[->flutter/lib/src/widgets/image.dart]

void _updateSourceStream(ImageStream newStream) {
  if (_imageStream?.key == newStream.key)
    return;

  if (_isListeningToStream)//初始为false
    _imageStream!.removeListener(_getListener());

  if (!widget.gaplessPlayback)//当ImageProvider改变是否还展示旧图片,默认为true
    setState(() { _replaceImage(info: null); });//将ImageInfo置空

  setState(() {
    _loadingProgress = null;
    _frameNumber = null;
    _wasSynchronouslyLoaded = false;
  });

  _imageStream = newStream;//保存当前的ImageStream
  if (_isListeningToStream)//初始为false
    _imageStream!.addListener(_getListener());
}

_ImageState._listenToStream

[->flutter/lib/src/widgets/image.dart]

void _listenToStream() {
  if (_isListeningToStream)//初始为false
    return;
  _imageStream!.addListener(_getListener());//为流增加监听,每个监听的ImageInfo为Compeleter中的clone
  _completerHandle?.dispose();
  _completerHandle = null;

  _isListeningToStream = true;
}

_ImageState._getListener

创建ImageStream的Listener

[->flutter/lib/src/widgets/image.dart]

ImageStreamListener _getListener({bool recreateListener = false}) {
  if(_imageStreamListener == null || recreateListener) {
    _lastException = null;
    _lastStack = null;
    //创建ImageStreamListener
    _imageStreamListener = ImageStreamListener(
      //处理ImageInfo回调
      _handleImageFrame,
      //字节流回调
      onChunk: widget.loadingBuilder == null ? null : _handleImageChunk,
      //错误回调
      onError: widget.errorBuilder != null
          ? (dynamic error, StackTrace? stackTrace) {
              setState(() {
                _lastException = error;
                _lastStack = stackTrace;
              });
            }
          : null,
    );
  }
  return _imageStreamListener!;
}

_ImageState._handleImageFrame

Listener中处理ImageInfo回调的部分

[->flutter/lib/src/widgets/image.dart]

void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
  setState(() {
    //图片加载完成,刷新Image组件 此ImageInfo中持有的image为原始数据的clone
    _replaceImage(info: imageInfo);
    _loadingProgress = null;
    _lastException = null;
    _lastStack = null;
    _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
    _wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
  });
}

_ImageState.build

注意绘制需要的是[pkg/sky_engine/lib/ui/painting.dart]下的Image类

[->flutter/lib/src/widgets/image.dart]

Widget build(BuildContext context) {
  if (_lastException  != null) {
    assert(widget.errorBuilder != null);
    return widget.errorBuilder!(context, _lastException!, _lastStack);
  }
	//使用RawImage展示_imageInfo?.image,如果image为空,则RawImage的大小为Size(0,0)
  //如果加载完成 则会被刷新和展示
  Widget result = RawImage(
    image: _imageInfo?.image,//解码后的图片数据
    debugImageLabel: _imageInfo?.debugLabel,
    width: widget.width,
    height: widget.height,
    scale: _imageInfo?.scale ?? 1.0,
    color: widget.color,
    colorBlendMode: widget.colorBlendMode,
    fit: widget.fit,
    alignment: widget.alignment,
    repeat: widget.repeat,
    centerSlice: widget.centerSlice,
    matchTextDirection: widget.matchTextDirection,
    invertColors: _invertColors,
    isAntiAlias: widget.isAntiAlias,
    filterQuality: widget.filterQuality,
  );
///...
  return result;
}

RawImage

[->flutter/lib/src/widgets/basic.dart]

Image控件其实只是负责图片源获取的逻辑处理,真正绘制图片的地方是RawImage

class RawImage extends LeafRenderObjectWidget

class RenderImage extends RenderBox

RenderImage继承自RenderBox,因此它需要提供自身的size。具体在performLayout中。

RenderImage.performLayout

[->flutter/lib/src/rendering/image.dart]

void performLayout() {
  size = _sizeForConstraints(constraints);
}

Size _sizeForConstraints(BoxConstraints constraints) {
    constraints = BoxConstraints.tightFor(
      width: _width,
      height: _height,
    ).enforce(constraints);

    if (_image == null)
      //Size(0,0)
      return constraints.smallest;
		//根据图片宽高等比缩放
    return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size(
      _image!.width.toDouble() / _scale,
      _image!.height.toDouble() / _scale,
    ));
  }

​ 可以看到当没有图片源的时候,大小为0,否则会根据约束和图片宽高来计算大小。

RenderImage.paint

RenderImage的绘制逻辑在paint方法中

[->flutter/lib/src/rendering/image.dart]

void paint(PaintingContext context, Offset offset) {
  if (_image == null)
    return;
  _resolve();
  assert(_resolvedAlignment != null);
  assert(_flipHorizontally != null);
  paintImage(
    canvas: context.canvas,
    rect: offset & size,
    image: _image!,
    debugImageLabel: debugImageLabel,
    scale: _scale,
    colorFilter: _colorFilter,
    fit: _fit,
    alignment: _resolvedAlignment!,
    centerSlice: _centerSlice,
    repeat: _repeat,
    flipHorizontally: _flipHorizontally!,
    invertColors: invertColors,
    filterQuality: _filterQuality,
    isAntiAlias: _isAntiAlias,
  );
}

​ paint中最后又调用到了一个Top-level方法paintImage来进行实际的绘制。paintImage方法很长,实际最终绘制是调用canvas.drawImageRect。至此,图片的加载到展示就完成了。

小结

​ Image的流程看似比较长,但是本质上就是获取图片源->解码->绘制的过程。

​ 我把大概流程整理成图,方便观看。

Flutter图片加载优化探索

内存优化

  • 按需清理ImageCache
  • 压缩内存中的Image尺寸 ​ 如果能够调整Image内存中的存储方式,比如将ARGB_8888的方式改为ARGB_4444或者RGB_565等,那么内存立省50%。可惜目前Flutter中还不支持这样存储的方式(目前支持的为rgba8888和bgra8888)。如果是混合开发,优化方式还有共享纹理(Texture)、共享Pointer等方式,这些方案实现起来会比较麻烦,我也没太多的去试验过,这里就不做过多讨论。

​ 当然以上都只是针对内存优化,针对网络图片我们可能还需要使用一层额外的磁盘缓存。需要注意的是,官方提供的NetworkImage是没实现磁盘缓存的。

按需清理ImageCache

​ 如果你认真阅读了上文的加载流程就绘制到,通过ImageProvider方式加载的图片,都会存在一份内存中的缓存。这是一个全局的图片缓存。

[->flutter/lib/src/painting/binding.dart]

void initInstances() {
  super.initInstances();
  _instance = this;
  _imageCache = createImageCache();//初始化图片缓存
  shaderWarmUp?.execute();
}

​ 在PaintingBIndinginitInstances方法中会初始化这个ImageCache,我们可以通过继承的方式替换掉这个全局的ImageCache,不过一般不需要这么做。

[->flutter/lib/src/painting/image_cache.dart]

class ImageCache {
  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
  final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{};
  int get maximumSize => _maximumSize;
  int _maximumSize = _kDefaultSize;
  ///...

ImageCache为我们提供了多级内存缓存,用来保存不同状态的图片流。下面简单介绍一下ImageCache的三种缓存类型。


ImageCache的三种缓存

  • _LiveImage

    此Cache用来保证流存活,创建时候会创建一个ImageStreamCompleterHandle,当流没有其Listener时候,会释放掉ImageStreamCompleterHandle,并从缓存map中移除。

class _LiveImage extends _CachedImageBase {
  _LiveImage(ImageStreamCompleter completer, VoidCallback handleRemove, {int? sizeBytes})
    //父类会创建`ImageStreamCompleterHandle`
      : super(completer, sizeBytes: sizeBytes) {
    _handleRemove = () {
      handleRemove();//从缓存map中移除自身
      dispose();
    };
    //Listener为空时候回调
    completer.addOnLastListenerRemovedCallback(_handleRemove);
  }

  late VoidCallback _handleRemove;

  @override
  void dispose() {
    completer.removeOnLastListenerRemovedCallback(_handleRemove);
    super.dispose();//释放`ImageStreamCompleterHandle`
  }

  @override
  String toString() => describeIdentity(this);
}
  • _CachedImage

    ​ 此Cache记录的是已经加载完的图片流

class _CachedImage extends _CachedImageBase {
  _CachedImage(ImageStreamCompleter completer, {int? sizeBytes})
    //会创建`ImageStreamCompleterHandle`保持流不被dispose
      : super(completer, sizeBytes: sizeBytes);
}
  • _PendingImage

    ​ 此Cache记录加载中的图片流。

class _PendingImage {
  _PendingImage(this.completer, this.listener);

  final ImageStreamCompleter completer;
  final ImageStreamListener listener;

  void removeListener() {
    completer.removeListener(listener);
  }
}
  • _CachedImage与_PendingImage的基类

    构造方法会创建ImageStreamCompleterHandle,dispose的时候会释放

    abstract class _CachedImageBase {
      _CachedImageBase(
        this.completer, {
        this.sizeBytes,
      }) : assert(completer != null),
      //创建`ImageStreamCompleterHandle`以保持流不被dispose
           handle = completer.keepAlive();
      final ImageStreamCompleter completer;
      int? sizeBytes;
      ImageStreamCompleterHandle? handle;
    
      @mustCallSuper
      void dispose() {
        assert(handle != null);
        // Give any interested parties a chance to listen to the stream before we
        // potentially dispose it.
        SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
          assert(handle != null);
          handle?.dispose();
          handle = null;
        });
      }
    }
    

Flutter图片加载优化探索

ImageCache提供最大图片缓存数量的设置方法,默认数量为1000,同时也提供了最大内存占用的设置,默认为100MB。同时还有基本的putIfAbsentevictclear方法。

​ 当我们想要降低内存占用的时候,我们可以按需清理ImageCache中存储的缓存。比如列表中的Image被dispose的时候,我们可以尝试移除它的缓存。大概用法如下:

@override
void dispose() {
  //..
  if (widget.evictCachedImageWhenDisposed) {
    _imagepProvider.obtainKey(ImageConfiguration.empty).then(
      (key) {
        ImageCacheStatus statusForKey =
            PaintingBinding.instance.imageCache.statusForKey(key);
        if (statusForKey?.keepAlive ?? false) {
          //只有已完成的evict
          _imagepProvider.evict();
        }
      },
    );
  }
  super.dispose();
}

​ 一般来说,ImageCache使用ImageProvider.obtainKey方法的返回值当做Key,当图片被dispose时候,我们获取到缓存的key,并从ImageCache中移除。

​ 需要注意的是,未完成加载的图片缓存不能清除。这是因为ImageStreamCompleter的实现类的构造方法中监听了异步加载的时间流,当异步加载完成后,会调用reportImageChunkEvent方法,此方法内部会调用_checkDisposed方法,此时如果图片流被dispose,则会抛出异常。

[->flutter/lib/src/painting/image_stream.dart]

bool _disposed = false;
void _maybeDispose() {
  //ImageStreamCompleter没有Listener也没有keepAliveHandle时,将会被释放
  if (!_hadAtLeastOneListener || _disposed || _listeners.isNotEmpty || _keepAliveHandles != 0) {
    return;
  }
	//释放Image
  _currentImage?.dispose();
  _currentImage = null;
  _disposed = true;
}

​ 清除内存缓存以换取内存的方式是一种以时间换空间的方式,图片展示将需要额外的加载和解码耗时,我们需要谨慎使用这种方式。

降低内存中的图片尺寸

​ 一张1920*1080尺寸的图片完整加载到内存中需要多大的内存呢?在Flutter中,图片数据一般会采用rgba_8888的方式存储。那么一个像素点的占用内存为4byte。则计算内存中的图片大小的公式如下:

imageWidth * imageHeight * 4

​ 通过代入公式我们可以知道1920*1080尺寸的图片完整加载后的大小为7833600byte,换算一下接近8MB。可以看到内存占用还是比较大的。如果列表中图片比较多,图片又没能及时释放,那么将会占用非常多的内存。

​ 在Android开发中,在把图片加载到内存中之前,我们可以通过BitmapFactory来加载原始图片的宽高数据,然后通过设置inSampleSize属性,降低图片的采样率,以达到降低内存占用的效果。在Flutter中,此方法的思想也是可行的。在原始图片被解码成Image数据之前,我们为其指定一个合适的尺寸,可以非常显著降低Image数据的内存占用。目前我的项目中也是采用了这种思路处理。

class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {
  const ResizeImage(
    this.imageProvider, {
    this.width,
    this.height,
    this.allowUpscaling = false,
  }) : assert(width != null || height != null),
       assert(allowUpscaling != null);

​ 官方其实已经为我们提供了一个ResizeImage来降低解码后的Image,但是它的缺陷是我们需要提前为Image指定宽或高,不够灵活。如果指定了宽或者高后,图片最后会被根据宽高按比例缩放

ResizeImage的实现原理并不复杂,他本身将成为传入的imageProvider的代理。如果我们指定了宽高,那么他将会代理原始ImageProvider完成图片的加载操作。

ImageStreamCompleter load(_SizeAwareCacheKey key, DecoderCallback decode) {
  final DecoderCallback decodeResize = (Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
   	//指定了cacheWidth 和 cacheHeight
    return decode(bytes, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling);
  };
  final ImageStreamCompleter completer = imageProvider.load(key.providerCacheKey, decodeResize);
  return completer;
}

​ 核心逻辑在load方法中,ResizeImage会为传入的DecoderCallback做一层装饰,为其传入cacheWidthcacheHeight尺寸。在上文的图片加载流程中,我也提到了,DecoderCallback的来源是PaintingBInding.instance.instantiateImageCodec。现在可以来看一下这里的实现:

[->flutter/lib/src/painting/binding.dart]

Future<ui.Codec> instantiateImageCodec(Uint8List bytes, {
  int? cacheWidth,
  int? cacheHeight,
  bool allowUpscaling = false,
}) {
  assert(cacheWidth == null || cacheWidth > 0);
  assert(cacheHeight == null || cacheHeight > 0);
  assert(allowUpscaling != null);
  //实际调用了ui.instantiateImageCodec
  return ui.instantiateImageCodec(
    bytes,
    targetWidth: cacheWidth,
    targetHeight: cacheHeight,
    allowUpscaling: allowUpscaling,
  );
}

​ 继续追踪源码:

[pkg/sky_engine/lib/ui/painting.dart]

Future<Codec> instantiateImageCodec(
  Uint8List list, {
  int? targetWidth,
  int? targetHeight,
  bool allowUpscaling = true,
}) async {
  final ImmutableBuffer buffer = await ImmutableBuffer.fromUint8List(list);
  //加载图片描述
  final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer);
  if (!allowUpscaling) {
    if (targetWidth != null && targetWidth > descriptor.width) {
      targetWidth = descriptor.width;
    }
    if (targetHeight != null && targetHeight > descriptor.height) {
      targetHeight = descriptor.height;
    }
  }
  //指定需要的宽高
  return descriptor.instantiateCodec(
    targetWidth: targetWidth,
    targetHeight: targetHeight,
  );
}

​ 这里我们可以看到cacheWidthcacheHeight实际影响到的是ImageDescriptortargetWidthtargetHeight属性。

​ 通过指定宽高,限制图片尺寸,内存占用会有一个很直观的改善。不过问题来了,官方的这个ResizeImage需要指定宽高,不够傻瓜不够好用咋办?

​ 这里我自己模仿ResizeImage的实现简单实现了一个AutoResizeImage,用AutoResizeImage包裹其他的ImageProvider默认即可达成压缩效果。可以指定压缩比例或者限制最大内存占用,默认为500KB。并且我也为extended_image开源库提交了PR,后续该库也会支持此特性。

​ 需要注意的是,降低图片的采样率后可能会出现图片显示模糊的情况。我们需要按需调整

转载自:https://juejin.cn/post/6940549272320344071
评论
请登录