在Flutter
的日常开发中,Image
是非常常见的一个控件。无论是加载资源图,还是网络图,都离不开此控件。如果使用的不好,在多大图的列表里,内存占用可能会出现非常恐怖,来个直观图的了解一下:
当列表中图片过多的时候,内存占用很轻松的飙升到了六七百MB,这是一个很夸张的数值,如果机器的配置不够,很可能就会因此而崩溃了。可见,图片加载的优化是非常重要的。我在做图片加载优化的版本是stable 1.22.6,当时优化完的效果大概如下图:
可以看到优化的效果还是很明显的。不过后续我的项目的Flutter
版本升级到了stable 2.0.x版本,我发现在新版本中官方对Image
控件和ImageCache
都做了一定的优化,所以目前来说我很推荐大家能够把项目的Flutter版本升级上去。
想要进行优化首先还是得了解一下基本原理。
Image加载流程
Image
本身是一个StatefulWidget
里,widget本身都是一些配置,状态相关的交互都在_ImageState
中。Image
自身为我们提供了数个构造,我们可以很方便的加载不同来源的图片。看了构造方法后我们就会知道,不管是那种构造方法,都离不开成员ImageProvider
。ImageProvider
的作用把不同来源的图片加载到内存中。
/// 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
实现类 MultiFrameImageStreamCompleter
。ImageStreamCompleter
的实现类还有一个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
RawImage
继承自LeafRenderObjectWidget
,通过RenderImage
来渲染图片。这里如果对RenderObject
不是很了解的话,可以看看我之前写过的一篇文章深入研究Flutter布局原理。
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 图片控件适配之路、flutter共享native资源的多种姿(fang)势(shi)等。在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();
}
在PaintingBInding
的initInstances
方法中会初始化这个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; }); } }
ImageCache
提供最大图片缓存数量的设置方法,默认数量为1000,同时也提供了最大内存占用的设置,默认为100MB。同时还有基本的putIfAbsent
、evict
、clear
方法。
当我们想要降低内存占用的时候,我们可以按需清理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
做一层装饰,为其传入cacheWidth
及cacheHeight
尺寸。在上文的图片加载流程中,我也提到了,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,
);
}
这里我们可以看到cacheWidth
和cacheHeight
实际影响到的是ImageDescriptor
的targetWidth
和targetHeight
属性。
通过指定宽高,限制图片尺寸,内存占用会有一个很直观的改善。不过问题来了,官方的这个ResizeImage
需要指定宽高,不够傻瓜不够好用咋办?
这里我自己模仿ResizeImage
的实现简单实现了一个AutoResizeImage,用AutoResizeImage包裹其他的ImageProvider
默认即可达成压缩效果。可以指定压缩比例或者限制最大内存占用,默认为500KB。并且我也为extended_image开源库提交了PR,后续该库也会支持此特性。
需要注意的是,降低图片的采样率后可能会出现图片显示模糊的情况。我们需要按需调整。