在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沒有堅挺著也沒有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,後續該庫也會支援此特性。
需要注意的是,降低圖片的取樣率後可能會出現圖片顯示模糊的情況。我們需要按需調整。