Image is a very common control in the daily development of Flutter. Whether load resource graph, or network graph, are inseparable from this control. If it is not used well, the memory footprint may appear very horrible in the list of large graphs. Let’s take a look at the graph to understand:

When there are too many images in the list, the memory usage can easily soar to 600 or 700 MB, which is an exaggerated number and will crash if the machine is underconfigured. As you can see, the optimization of image loading is very important. The version OF image loading optimization I am doing is Stable 1.22.6, and the optimized effect is as follows:

You can see that the optimization effect is still very obvious. However, my project’s Flutter version was upgraded to Stable 2.0.x. I found that the Image control and ImageCache were optimized in the new version. Therefore, I strongly recommend you to upgrade the Flutter version of your project.

To optimize, you need to understand the fundamentals.

Image loading process

The Image itself is a StatefulWidget, the widget itself is a configuration, and the state-related interactions are in _ImageState. Image itself provides us with several constructs that make it easy to load images from different sources. When we look at the constructor, we know that no constructor is dependent on the member ImageProvider. The purpose of ImageProvider is to load images from different sources into memory.

/// The image to display.
final ImageProvider image;
Copy the code

Let’s start by analyzing how an image is loaded and displayed.

_ImageState.didChangeDependencies

The loading logic for an Image starts with the didChangeDependencies method.

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

void didChangeDependencies() {
  _updateInvertColors();
  _resolveImage();/ / ImageProvider processing

  if (TickerMode.of(context))// Whether ticker is enabled. The default value is true
    _listenToStream();/ / to monitor flow
  else
    _stopListeningToStream(keepStreamAlive: true);

  super.didChangeDependencies();
}
Copy the code

_ImageState._resolveImage

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

void _resolveImage() {
  // Prevent a quick slide-loaded wrapper from wrapping the ImageProvider created in the Widget
  final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
    context: _scrollAwareContext,
    imageProvider: widget.image,
  );
  final ImageStream newStream =
    / / create ImageStreamprovider.resolve(createLocalImageConfiguration( context, size: widget.width ! =null&& widget.height ! =null? Size(widget.width! , widget.height!) :null));assert(newStream ! =null);
  / / update the flow
  _updateSourceStream(newStream);
}
Copy the code

ImageProvider.resolve

Create the flow and stream for ImageStream and set the ImageStreamCompleter callback.

[->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) {
      // Try setting ImageStreamCompleter for stream
      resolveStreamForKey(configuration, stream, key, errorHandler);
    },
    (T? key, Object exception, StackTrace? stack) async {
      ///.});return stream;
}
Copy the code

ImageProvider.resolveStreamForKey

Try to set an instance of ImageSreamCompleter for the created ImageStream

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

void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
  	if(stream.completer ! =null) {
    finalImageStreamCompleter? completer = PaintingBinding.instance! .imageCache! .putIfAbsent( key, () => stream.completer! , onError: handleError, );assert(identical(completer, stream.completer));
    return;
  }
  // Cache
  finalImageStreamCompleter? completer = PaintingBinding.instance! .imageCache! .putIfAbsent( key,// This closure calls the imageprovider.load method
    // Note that the second argument to the load method is PaintingBinding. Instance! .instantiateImageCodec() => load(key, PaintingBinding.instance! .instantiateImageCodec), onError: handleError, );if(completer ! =null) { stream.setCompleter(completer); }}Copy the code

ImageCache.putIfAbsent

Try to put the request into the global cache ImageCache and set up a listener

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

ImageStreamCompleter? putIfAbsent(Objectkey, ImageStreamCompleter loader(), { ImageErrorListener? onError }) { ImageStreamCompleter? result = _pendingImages[key]? .completer;// Null if it is the first load
  if(result ! =null) {
    return result;
  }
  final _CachedImage? image = _cache.remove(key);
    // Null if it is the first load
  if(image ! =null) {
    // Keep the ImageStream alive and save it to the active map
    _trackLiveImage(
      key,
      image.completer,
      image.sizeBytes,
    );
    // Cache this Image
    _cache[key] = image;
    return image.completer;
  }
  final _LiveImage? liveImage = _liveImages[key];
   // Null if it is the first load
  if(liveImage ! =null) {
    // The _LiveImage stream may have completed, provided sizeBytes is not empty
    // If not, aliveHandler created by _CachedImage is released
    _touch(
      key,
      _CachedImage(
        liveImage.completer,
        sizeBytes: liveImage.sizeBytes,
      ),
      timelineTask,
    );
    return liveImage.completer;
  }

  try {
    result = loader();// If the cache misses, the imageprovider.load method is called
    _trackLiveImage(key, result, null);// Ensure that streams are not disposed
  } 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;
      // Each Listener causes the imageInfo. image reference count to be +1, and the image cannot be freed if it is not freed. Release the processing of this _Image
      info.dispose();
    }
    // Active count +1
    final_CachedImage image = _CachedImage( result! , sizeBytes: sizeBytes, );// Active count +1 May also be ignored
    _trackLiveImage(key, result, sizeBytes);
    if (untrackedPendingImage == null) {
      // If caching is allowed, cache _CachedImage
      _touch(key, image, listenerTask);
    } else {
      // Release the image directly
      image.dispose();
    }
    final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
    if(pendingImage ! =null) {
      // Remove the listener for the loading image, if it is the last one, the _LiveImage will also be released
      pendingImage.removeListener();
    }
    listenedOnce = true;
  }

  final ImageStreamListener streamListener = ImageStreamListener(listener);
  if (maximumSize > 0 && maximumSizeBytes > 0) {
    // Store the loaded map
    _pendingImages[key] = _PendingImage(result, streamListener);
  } else {
    // If the cache is not set, a field will be used to prevent memory leaks caused by the previous _LiveImage saving
    untrackedPendingImage = _PendingImage(result, streamListener);
  }
  // Go here and register a listener for the compeleter returned by the imageProvider.load method.
  result.addListener(streamListener);// If imagestReAMCompleter. _currentImage is not empty, it is immediately called back
  return result;
}
Copy the code

ImageProvider.load

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

ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
  // Create a asynchronously loaded event flow controller
  final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
	// Create the actual ImageCompleter implementation class
  return MultiFrameImageStreamCompleter(
    // Image decoding callback
    codec: _loadAsync(key as NetworkImage, chunkEvents, decode),// Asynchronous loading method
    // Asynchronously loaded streams
    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), ]; }); }Copy the code

ImageProvider method of this method is abstract, NetworkProvider, for example, here will create asynchronous loading time flow controller, and create the actual ImageStreamCompleter MultiFrameImageStreamCompleter implementation class. ImageStreamCompleter implementation class and a OneFrameImageStreamCompleter, but in the current official source and use.

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);
		// Initiate a network request using HttpClient
    final HttpClientRequest request = await _httpClient.getUrl(resolved);
		// You can configure your own headersheaders? .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 is converted to a byte array
    final Uint8List bytes = await consolidateHttpClientResponseBytes(
      response,
      onBytesReceived: (int cumulative, int? total) {
        // Send the data stream EventchunkEvents.add(ImageChunkEvent( cumulativeBytesLoaded: cumulative, expectedTotalBytes: total, )); });if (bytes.lengthInBytes == 0)
      throw Exception('NetworkImage is an empty file: $resolved');
		// Use DecoderCallback to process raw data
    return decode(bytes);
  } catch(e) { scheduleMicrotask(() { PaintingBinding.instance! .imageCache! .evict(key); });rethrow;
  } finally{ chunkEvents.close(); }}Copy the code

This method is the way to actually load the image source data, and different data sources will have different logic. The essence is to retrieve the raw bytes of the image and then use DecoderCallback to process the raw data back. DecoderCallback is a PaintingBinding. Instance! InstantiateImageCodec.

_ImageState._updateSourceStream

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

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

  if (_isListeningToStream)// Start with false_imageStream! .removeListener(_getListener());if(! widget.gaplessPlayback)// When the ImageProvider changes whether to show the old image, the default is true
    setState(() { _replaceImage(info: null); });// Empty ImageInfo

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

  _imageStream = newStream;// Save the current ImageStream
  if (_isListeningToStream)// Start with false_imageStream! .addListener(_getListener()); }Copy the code

_ImageState._listenToStream

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

void _listenToStream() {
  if (_isListeningToStream)// Start with false
    return; _imageStream! .addListener(_getListener());// Add a listener for the stream, each listener's ImageInfo is clone in Compeleter_completerHandle? .dispose(); _completerHandle =null;

  _isListeningToStream = true;
}
Copy the code

_ImageState._getListener

Create a Listener for ImageStream

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

ImageStreamListener _getListener({bool recreateListener = false{})if(_imageStreamListener == null || recreateListener) {
    _lastException = null;
    _lastStack = null;
    / / create ImageStreamListener
    _imageStreamListener = ImageStreamListener(
      // Handle the ImageInfo callback
      _handleImageFrame,
      // byte stream callback
      onChunk: widget.loadingBuilder == null ? null : _handleImageChunk,
      // Error callbackonError: widget.errorBuilder ! =null
          ? (dynamic error, StackTrace? stackTrace) {
              setState(() {
                _lastException = error;
                _lastStack = stackTrace;
              });
            }
          : null,); }return_imageStreamListener! ; }Copy the code

_ImageState._handleImageFrame

The part of the Listener that handles the ImageInfo callback

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

void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
  setState(() {
    // The Image in ImageInfo is clone of the original data
    _replaceImage(info: imageInfo);
    _loadingProgress = null;
    _lastException = null;
    _lastStack = null;
    _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
    _wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
  });
}
Copy the code

_ImageState.build

Need pay attention to the drawing is [PKG/sky_engine/lib/UI/painting. The dart] under the Image class

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

Widget build(BuildContext context) {
  if(_lastException ! =null) {
    assert(widget.errorBuilder ! =null);
    returnwidget.errorBuilder! (context, _lastException! , _lastStack); }// Use RawImage to display _imageInfo? .image, if image is empty, then RawImage is Size(0,0).
  // If the load is complete, it will be refreshed and displayedWidget result = RawImage( image: _imageInfo? .image,// Decoded image datadebugImageLabel: _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;
}
Copy the code

RawImage

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

The Image control is only responsible for the logical processing of Image source acquisition. The real drawing place is RawImage.

class RawImage extends LeafRenderObjectWidget
Copy the code

RawImage inherits LeafRenderObjectWidget and renders images through RenderImage. If you’re not familiar with RenderObject here, take a look at an article I wrote earlier that delved into the Flutter layout.

class RenderImage extends RenderBox
Copy the code

RenderImage inherits from RenderBox, so it needs to provide its own size. This is in 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)
      / / the Size (0, 0)
      return constraints.smallest;
		// Zoom according to the width and height ratio of the image
    returnconstraints.constrainSizeAndAttemptToPreserveAspectRatio(Size( _image! .width.toDouble() / _scale, _image! .height.toDouble() / _scale, )); }Copy the code

You can see that the size is 0 when there is no image source, otherwise the size is calculated based on constraints and image width.

RenderImage.paint

The rendering logic for RenderImage is in the paint method

[->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, ); }Copy the code

In paint, a top-level method called paintImage is finally called to do the actual drawing. The paintImage method is very long, and the actual final drawing is a call to canvas.drawImageRect. At this point, the loading of the image to display is complete.

summary

The process of Image seems to be quite long, but in essence, it is the process of obtaining Image source -> decoding -> drawing.

I put the general process into a picture, easy to watch.

Memory optimization

After analyzing the loading process, we can discuss the memory optimization scheme. Before making optimization, I also referred to some articles, such as image list memory optimization, the adaptive path of Flutter image controls, multiple poses (FANG) potential (SHI) of Flutter sharing native resources, etc. Currently, I can think of the following possible optimization directions in the Flutter layer:

  • Clean up the ImageCache as needed
  • Compress the Image size in memory

If you can change the way the Image memory is stored, such as ARGB_8888 to ARGB_4444 or RGB_565, you can save 50%. Unfortunately, Flutter does not currently support this storage (rgba8888 and bgra8888 are currently supported). If it’s mixed development, the optimization methods include shared Texture, shared Pointer, etc. These methods can be quite troublesome to implement. I haven’t tried them too much, so I won’t discuss them too much here.

Of course, this is only for memory optimization, and we may need to use an additional layer of disk caching for network images. Note that NetworkImage does not implement disk caching.

Clean up the ImageCache as needed

If you read the loading process carefully, you can draw that all images loaded through ImageProvider are stored in an in-memory cache. This is a global image cache.

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

void initInstances() {
  super.initInstances();
  _instance = this;
  _imageCache = createImageCache();// Initialize the image cacheshaderWarmUp? .execute(); }Copy the code

The ImageCache is initialized in the initInstances method of the PaintingBInding. We can replace the global ImageCache by inheritance, but we generally do not need to.

[->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;
  ///.
Copy the code

ImageCache provides us with a multi-level memory cache for storing image streams in different states. Here’s a quick look at the three cache types for ImageCache.


Three caches for ImageCache

  • _LiveImage

    When the Cache is used to ensure the flow, create creates a ImageStreamCompleterHandle, when flow without the Listener, would release ImageStreamCompleterHandle, and removed from the Cache map.

class _LiveImage extends _CachedImageBase {
  _LiveImage(ImageStreamCompleter completer, VoidCallback handleRemove, {int? sizeBytes})
    / / superclass creates ` ImageStreamCompleterHandle `
      : super(completer, sizeBytes: sizeBytes) {
    _handleRemove = () {
      handleRemove();// Remove itself from the cache map
      dispose();
    };
    //Listener is an empty callback
    completer.addOnLastListenerRemovedCallback(_handleRemove);
  }

  late VoidCallback _handleRemove;

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

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

    This Cache records the image stream that has been loaded

class _CachedImage extends _CachedImageBase {
  _CachedImage(ImageStreamCompleter completer, {int? sizeBytes})
    / / creates ` ImageStreamCompleterHandle ` keep flow not dispose
      : super(completer, sizeBytes: sizeBytes);
}
Copy the code
  • _PendingImage

    This Cache records the stream of images being loaded.

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

  final ImageStreamCompleter completer;
  final ImageStreamListener listener;

  voidremoveListener() { completer.removeListener(listener); }}Copy the code
  • Base class for _CachedImage and _PendingImage

    Constructor creates ImageStreamCompleterHandle, will release the dispose

    abstract class _CachedImageBase {
      _CachedImageBase(
        this.completer, {
        this.sizeBytes,
      }) : assert(completer ! =null),
      / / create a ` ImageStreamCompleterHandle ` to keep flow are not 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; }); }}Copy the code

ImageCache provides the maximum number of images cached (1000 by default) and the maximum memory usage (100MB by default). There are also basic putIfAbsent, EVICT, and Clear methods.

When we want to reduce our memory footprint, we can clean up the cache stored in ImageCache as needed. For example, when dispose of the Image in the list, we can try to remove its cache. The usage is as follows:

@override
void dispose() {
  / /..
  if (widget.evictCachedImageWhenDisposed) {
    _imagepProvider.obtainKey(ImageConfiguration.empty).then(
      (key) {
        ImageCacheStatus statusForKey =
            PaintingBinding.instance.imageCache.statusForKey(key);
        if(statusForKey? .keepAlive ??false) {
          // Only evICT completed_imagepProvider.evict(); }}); }super.dispose();
}
Copy the code

Normally, ImageCache uses the return value of the ImageProvider.obtainKey method as the Key. When dispose the image, we grab the cached Key and remove it from the ImageCache.

Note that unfinished image caches cannot be cleared. This is because the constructor of the implementation class of ImageStreamCompleter listens for the time stream of the asynchronous load, and when the asynchronous load is complete, the reportImageChunkEvent method is called, and inside that method is called the _checkDisposed method, Dispose will throw an exception if the image stream is disposed.

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

bool _disposed = false;
void _maybeDispose() {
  //ImageStreamCompleter with no Listener and no keepAliveHandle will be released
  if(! _hadAtLeastOneListener || _disposed || _listeners.isNotEmpty || _keepAliveHandles ! =0) {
    return;
  }
	/ / release the Image_currentImage? .dispose(); _currentImage =null;
  _disposed = true;
}
Copy the code

Clearing the memory cache in exchange for memory is a time-for-space approach, and the image display will require additional loading and decoding time, so we need to use this approach carefully.

Reduce the size of images in memory

How much memory does it take for a 1920 by 1080 image to load fully into memory? Image data is stored in Flutter as RGBA_8888. So the memory footprint of a pixel is 4 bytes. Then the formula to calculate the size of the picture in memory is as follows:

imageWidth * imageHeight * 4

By plugging in the formula, we know that the image size of 1920*1080 is 7833600bytes after full loading, which is close to 8MB. You can see that the memory footprint is still quite large. If there are too many images in the list and the images are not released in time, it will take up a lot of memory.

In Android development, before loading images into memory, we can use BitmapFactory to load the width and height data of the original image, and then set the inSampleSize property to reduce the image sampling rate to achieve the effect of reducing memory usage. The idea of this method also works in Flutter. Before the original Image is decoded into Image data, we specify an appropriate size for it, which can significantly reduce the memory footprint of Image data. At present, this approach is also adopted in my project.

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

In fact, the official has provided us with a ResizeImage to reduce the decoded Image, but its defect is that we need to specify the width or height of the Image in advance, which is not flexible enough. If width or height is specified, the image will be scaled accordingly.

The implementation principle of ResizeImage is not complicated, it will itself become the proxy of the incoming imageProvider. If we specify the width and height, it will load the image for the original ImageProvider.

ImageStreamCompleter load(_SizeAwareCacheKey key, DecoderCallback decode) {
  final DecoderCallback decodeResize = (Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
   	// cacheWidth and cacheHeight are specified
    return decode(bytes, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling);
  };
  final ImageStreamCompleter completer = imageProvider.load(key.providerCacheKey, decodeResize);
  return completer;
}
Copy the code

In the load method, ResizeImage decorates the passed DecoderCallback with cacheWidth and cacheHeight. In the above image loading process, I also mentioned, DecoderCallback PaintingBInding. Is the source of the instance. InstantiateImageCodec. Now we can look at the implementation here:

[->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 is actually called
  return ui.instantiateImageCodec(
    bytes,
    targetWidth: cacheWidth,
    targetHeight: cacheHeight,
    allowUpscaling: allowUpscaling,
  );
}
Copy the code

Continue to trace the source:

[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);
  // Load the image description
  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; }}// Specify the desired width and height
  return descriptor.instantiateCodec(
    targetWidth: targetWidth,
    targetHeight: targetHeight,
  );
}
Copy the code

Here we can see that cacheWidth and cacheHeight actually affect the targetWidth and targetHeight properties of the ImageDescriptor.

By specifying the width and height and limiting the image size, the memory footprint can be improved intuitively. However, the problem is that the official ResizeImage needs to be specified with width and height. What should we do if it is not stupid enough to use?

Here, I simply realized an AutoResizeImage by imitating the implementation of ResizeImage, and wrapped other ImageProviders with AutoResizeImage to achieve compression effect by default. You can specify the compression ratio or limit the maximum memory usage, which is 500KB by default. I have also submitted PR for the Extended_image open source library, which will support this feature in the future.

It should be noted that the image display may be blurred after the image sampling rate is reduced. We need to adjust as needed.