Flutter圖片載入優化深入探索

RayC發表於2021-03-17

​ 在Flutter的日常開發中,Image是非常常見的一個控制元件。無論是載入資源圖,還是網路圖,都離不開此控制元件。如果使用的不好,在多大圖的列表裡,記憶體佔用可能會出現非常恐怖,來個直觀圖的瞭解一下:

image-20210310212231175

​ 當列表中圖片過多的時候,記憶體佔用很輕鬆的飆升到了六七百MB,這是一個很誇張的數值,如果機器的配置不夠,很可能就會因此而崩潰了。可見,圖片載入的優化是非常重要的。我在做圖片載入優化的版本是stable 1.22.6,當時優化完的效果大概如下圖:

image-20210310213909407

​ 可以看到優化的效果還是很明顯的。不過後續我的專案的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
複製程式碼

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的流程看似比較長,但是本質上就是獲取圖片源->解碼->繪製的過程。

​ 我把大概流程整理成圖,方便觀看。

image-20210317094330466

記憶體優化

​ 分析完載入流程我們可以來探討一下記憶體優化的方案了。我在做優化之前也參考了一些文章,諸如圖片列表記憶體優化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();
}
複製程式碼

​ 在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;
        });
      }
    }
    複製程式碼

image-20210317162601203

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沒有堅挺著也沒有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,後續該庫也會支援此特性。

​ 需要注意的是,降低圖片的取樣率後可能會出現圖片顯示模糊的情況。我們需要按需調整

相關文章