preface

Image is a widget that Flutter uses to display images. It can load web, local, file, or in-memory images in JPEG, PNG, GIF, animated GIF, WebP, animated WebP, BMP, and WBMP formats. The Flutter Image itself also implements memory caching, which can greatly improve the speed of Image display.

Review how Image opens

  • Image.network

    Image.network("Picture address",fit: BoxFit.cover,width: ,height: 400)
    Copy the code
  • Image.file

    Image.file(File("Local Image Path"));
    Copy the code
  • Image.asset

    Image.asset("Image resources in the project need to be declared in the pubspec.yanl file");
    Copy the code
  • Image.memory

    Image.memory(Uint8List.fromList([]));
    Copy the code

    You need to pass in an array of bytes

The resolution of the Flutter loading Image

Flutter can load images of the appropriate resolution to the current device, and assign images of different resolutions as shown below:

The default resolution of the main resource is 1.0x. If the resolution is greater than 1.0, the image file under 2.0x will be selected. Images in Flutter must be declared in pubspec.yaml, as shown below:

flutter:
  uses-material-design: true
  assets:
    - images/icon.png
    - 2.0 x/images/icon. PNG
    - 3.0 x/images/icon. PNG
    - 4.0 x/images/icon. PNG
Copy the code

Every image in the pubspec.yaml file must correspond to the actual file. Correspondingly, when the main resource image is missing, it is loaded from the highest resolution order.

Flutter packaging applications, resources will be in accordance with the key – value form in apk assets/flutter_assets/AssetManifest josn file, load resources will parse the file, select the most appropriate to load display file. The details are as follows:

Flutter.network source code analysis

Before you start, it’s good to look at some classes and come back to them when the process is over:

  • Image: Used to display images
  • _ImageState: The state class of the Image, which handles the life cycle and calls loading.
  • ImageProvider: ImageProvider used to load images, such as NetWrokImage, ResizeImage, etc.
  • ImageStreamCompleter: Image resource management class
  • ImageStream:ImageStreamIs a handle to an image resource, which holds the image resource, the callback after loading, and the image resource manager. And one of theImageStreamCompleter Object is the image resource management class
  • The parser MultiFrameImageStreamCompleter: frame images
  • ImageStreamListener: Listens to the loading result of the image. After the loading is complete, a callback is performed, and then the page is refreshed to display the image

Let’s start the process:

Image.network(
  String src, {
  Key? key,
  ...///
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),;
Copy the code

When an Image object is created using Image.network, the instance variable Image is initialized.

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

Image is actually the ImageProvider image provider, which itself is an abstract class, with the following subclasses:

The NetWorkImage class is used to load network images.

Let’s look at the Image component directly. The Image component itself is a StatefulWidget whose state is managed by _ImageState. From the State lifecycle, we know that the initState method is executed first

initState

@override
void initState() {
  super.initState();
  // Add listening for system Settings, such as screen rotation, etcWidgetsBinding.instance! .addObserver(this);
  // Provide non-leak access to BuildContext
  _scrollAwareContext = DisposableBuildContext<State<Image>>(this);
}
Copy the code

didChangeDependencies

Upon completion of initState(), didChangeDependencies() is executed as follows:

@override
void didChangeDependencies() {
  _updateInvertColors();
  _resolveImage();///Resolution images

  if (TickerMode.of(context))
    _listenToStream();
  else
    _stopListeningToStream(keepStreamAlive: true);

  super.didChangeDependencies();
}
Copy the code
ImageState._resolveImage
void _resolveImage() {
  / / ScrollAwareImageProvider can avoid loading picture when rolling
  final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
    context: _scrollAwareContext,
    imageProvider: widget.image,
  );
  ///Create the ImageStream object
  final ImageStream newStream =
    // The ImageProvder 'resolve' method is called. As follows:provider.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
ImageProvder.resolve
ImageStream resolve(ImageConfiguration configuration) {
  assert(configuration ! =null);
  / / create ImageStream
  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 the ImageStreamCompleter for the Stream
      resolveStreamForKey(configuration, stream, key, errorHandler);
    },
    (T? key, Object exception, StackTrace? stack) async{});return stream;
}
ImageStream createStream(ImageConfiguration configuration) {
   return ImageStream();
}
Copy the code

The above code creates the ImageStream and sets the ImageStreamCompleter callback.

ImageStream is a handle to an image resource, which holds the image resource, the callback after loading, and the image resource manager. The ImageStreamCompleter object is the image resource management class

resolveStreamForKey
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
  ///If it is not empty, the stream is finished, which is directly cached
  if(stream.completer ! =null) {
    ///Deposited in the cache
    finalImageStreamCompleter? completer = PaintingBinding.instance! .imageCache! .putIfAbsent( key, () => stream.completer! , onError: handleError, );assert(identical(completer, stream.completer));
    return;
  }
  /// If the stream is not complete, it is cached and the load method for loading the image is passed in as well.
  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, );// Finally set ImageStreamCompleter to ImageStream
  if(completer ! =null) { stream.setCompleter(completer); }}Copy the code

The above code attempts to set up an instance of ImageStreamCompleter for the created ImageStream.

_ImageState through

ImageCache.putIfAbsent
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) { ImageStreamCompleter? result = _pendingImages[key]? .completer;// If this is the first load, result == null
  if(result ! =null) {
    return result;
  }

  // If it is the first load, image == null
  final _CachedImage? image = _cache.remove(key);
  if(image ! =null) {
    // Keep the ImageStream alive and save it to an active map
    _trackLiveImage(
      key,
      image.completer,
      image.sizeBytes,
    );
    // Cache this Image
    _cache[key] = image;
    return image.completer;
  }

  final _LiveImage? liveImage = _liveImages[key];
  // If it is the first load, liveImage == null
  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 {
    // If there is none in the cache, the imageprovider.load method is called
    result = loader();
    // Ensure that streams are not disposed
    _trackLiveImage(key, result, null);
  } catch (error, stackTrace) {
    if(! kReleaseMode) {if(onError ! =null) {
      onError(error, stackTrace);
      return null;
    } else {
      rethrow; }}bool listenedOnce = false;
      
  _PendingImage? untrackedPendingImage;
  void listener(ImageInfo? info, bool syncCall) {
    int? sizeBytes;
    if(info ! =null) {
      sizeBytes = info.sizeBytes;
      // Each Listener causes the imageInfo. image reference count to be +1, and the image cannot be freed if it is not freed.
      // Release this _Image processing
      info.dispose();
    }
    // Active count +1
    final_CachedImage image = _CachedImage( result! , sizeBytes: sizeBytes, );// Active count +1, also possible to ignore
    _trackLiveImage(key, result, sizeBytes);

    // Only touch if the cache was enabled when resolve was initially called.
    if (untrackedPendingImage == null) {
      _touch(key, image, listenerTask);
    } else {
      // Release the image directly
      image.dispose();
    }

    final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
    if(pendingImage ! =null) {
      /// Remove the loading picture listener, times if it is the last, then_LiveImage will also be released
      pendingImage.removeListener();
    }
    listenedOnce = true;
  }

  final ImageStreamListener streamListener = ImageStreamListener(listener);
  if (maximumSize > 0 && maximumSizeBytes > 0) {
    /// Map saved to load
    _pendingImages[key] = _PendingImage(result, streamListener);
  } else {
    /// A field save is also called without caching, preventing previous saves of _Memory leak caused by LiveImage
    untrackedPendingImage = _PendingImage(result, streamListener);
  }
  // The imageprovider.load method returns the Completer registration listener
  result.addListener(streamListener);
 
  return result;
}
Copy the code

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

ImageProvider.load
@override
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
    // Ownership of this controller is handed off to [_loadAsync]; it is that
    // method's responsibility to close the controller's stream when the image
    // has been loaded or an error is thrown.
    final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
      /// Asynchronous loading method
      codec: _loadAsync(key as NetworkImage, chunkEvents, decode),
      /// Asynchronous load listening
      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

The load method is abstract. The load method here is the implementation class NetWorkImage. This code creates a MultiFrameImageStreamCompleter object and returns, this is more than one frame image manager, suggests Fluter support GIF images. The codec variable that creates the object is initialized by the return value of the _loadAsync method

NetworkImage._loadAsync
 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);

      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) {await response.drain<List<int> > ();throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
      }

      final Uint8List bytes = await consolidateHttpClientResponseBytes(
        response,
        onBytesReceived: (int cumulative, int?total) { chunkEvents.add(ImageChunkEvent( cumulativeBytesLoaded: cumulative, expectedTotalBytes: total, )); });if (bytes.lengthInBytes == 0)
        throw Exception('NetworkImage is an empty file: $resolved');

      return decode(bytes);
    } catch(e) { scheduleMicrotask(() { PaintingBinding.instance! .imageCache! .evict(key); });rethrow;
    } finally{ chunkEvents.close(); }}Copy the code

This method is to download the image source data operation, different data sources will have different logic. After downloading, instantiate the image decoder object Codec based on the binary data of the image and return. Next we see MultiFrameImageStreamCompleter class.

MultiFrameImageStreamCompleter
MultiFrameImageStreamCompleter({
  required Future<ui.Codec> codec,
  required double scale,
  String? debugLabel,
  Stream<ImageChunkEvent>? chunkEvents,
  InformationCollector? informationCollector,
}) : assert(codec ! =null),
     _informationCollector = informationCollector,
     _scale = scale {
  this.debugLabel = debugLabel;
  codec.then<void>(_handleCodecReady, onError: (Object error, StackTrace stack) {
    reportError(
      context: ErrorDescription('resolving an image codec'),
      exception: error,
      stack: stack,
      informationCollector: informationCollector,
      silent: true,); });if(chunkEvents ! =null) {
    chunkEvents.listen(reportImageChunkEvent,
      onError: (Object error, StackTrace stack) {
        reportError(
          context: ErrorDescription('loading an image'),
          exception: error,
          stack: stack,
          informationCollector: informationCollector,
          silent: true,); }); }void _handleCodecReady(ui.Codec codec) {
    _codec = codec;
    assert(_codec ! =null);

    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
  }
         
  Future<void> _decodeNextFrameAndSchedule() async {
    // This will be null if we gave it away. If not, it's still ours and it
    // must be disposed of._nextFrame? .image.dispose(); _nextFrame =null;
    try {
      _nextFrame = await_codec! .getNextFrame(); }catch (exception, stack) {
      reportError(
        context: ErrorDescription('resolving an image frame'),
        exception: exception,
        stack: stack,
        informationCollector: _informationCollector,
        silent: true,);return;
    }
    if(_codec! .frameCount ==1) {

      if(! hasListeners) {return; } _emitFrame(ImageInfo( image: _nextFrame! .image.clone(), scale: _scale, debugLabel: debugLabel, )); _nextFrame! .image.dispose(); _nextFrame =null;
      return; } _scheduleAppFrame(); }}Copy the code

Codec of the asynchronous method will be called after the completion of execution _handleCodecReady function, methods of codec object will be stored, and then call _decodeNextFrameAndSchedule decoded picture frames.

If the image is not in animation format, the _emitFrame function is executed to take the image frame object from the frame data and create an ImageInfo object based on the scale, and then set the image information

void _emitFrame(ImageInfo imageInfo) {
  setImage(imageInfo);
  _framesEmitted += 1;
}
  @protected
  @pragma('vm:notify-debugger-on-exception')
  voidsetImage(ImageInfo image) { _checkDisposed(); _currentImage? .dispose(); _currentImage = image;if (_listeners.isEmpty)
      return;
    // Make a copy to allow for concurrent modification.
    final List<ImageStreamListener> localListeners =
        List<ImageStreamListener>.from(_listeners);
    for (final ImageStreamListener listener in localListeners) {
      try {
        listener.onImage(image.clone(), false);
      } catch (exception, stack) {
        reportError(
          context: ErrorDescription('by an image listener'), exception: exception, stack: stack, ); }}}Copy the code

At this point, the listener is notified that a new image needs to be rendered. So when is this listener added? Let’s go back to the didChangeDependencies method. After the _resolveImage method is executed, the _listenToStream method is executed.

ImageState.__updateSourceStream

Update _imageStream to newStream, and move the flow listener registration from the old stream to the newStream (if a listener is registered).

NewStream is the ImageStream created in the _resolveImage() method.

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

  if (_isListeningToStream)///The original is false_imageStream! .removeListener(_getListener());if(! widget.gaplessPlayback)// The old image is displayed when the ImageProvider changes, which defaults to true
    setState(() { _replaceImage(info: null); }); // Empty ImageInfo

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

  _imageStream = newStream; // Save the current ImageStream
  if (_isListeningToStream) /// The original is false_imageStream! .addListener(_getListener()); }Copy the code
ImageState._listenToStream

In the above MultiFrameImageStreamCompleter class, the picture is encapsulated into ImageInfo object after processing is completed, then by adding listeners to notice.

The listener is added via the _listenToStream method. The _listenToStream method is executed after the _resolveImage method in the didChangeDependencies method completes execution. Details are as follows:

void _listenToStream() {
  if (_isListeningToStream) // Start with false
    return;
  ///Add a listener to the stream, with the ImageInfo of each listener being clone in the Completer_imageStream! .addListener(_getListener()); _completerHandle? .dispose(); _completerHandle =null;

  _isListeningToStream = true;
}
Copy the code

This method adds a listener to the _imageStream object, which is retrieved via _getListener.

ImageState._getListener
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 correctiononError: widget.errorBuilder ! =null || kDebugMode
          ? (Object error, StackTrace? stackTrace) {
              setState(() {
                _lastException = error;
                _lastStack = stackTrace;
              });
              assert(() {
                if (widget.errorBuilder == null)
                  throw error; // Ensures the error message is printed to the console.
                return true; } ()); } :null,); }return_imageStreamListener! ; }Copy the code

Create a Listener for ImageStream.

ImageState._handleImageFrame

The part of the Listener that handles the ImageInfo callback, which is called when a new one needs to be rendered, and finally the setState() method is called to notify the interface of a refresh

void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
  setState(() {
    ///After the Image is loaded, refresh the Image component. The Image held in this ImageInfo is the 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

build

Widget build(BuildContext context) {
  if(_lastException ! =null) {
    if(widget.errorBuilder ! =null)
      returnwidget.errorBuilder! (context, _lastException! , _lastStack);if (kDebugMode)
      return_debugBuildErrorWidget(context, _lastException!) ; }// 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 displayed
  Widget result = RawImage(
    // Do not clone the image, because RawImage is a stateless wrapper.
    // The image will be disposed by this state object when it is not needed
    // anymore, such as when it is unmounted or when the image stream pushes
    // a new image.image: _imageInfo? .image,// Decoded image datadebugImageLabel: _imageInfo? .debugLabel, width: widget.width, height: widget.height, scale: _imageInfo?.scale ??1.0,
    color: widget.color,
    opacity: widget.opacity,
    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,
  );

  if(! widget.excludeFromSemantics) { result = Semantics( container: widget.semanticLabel ! =null,
      image: true,
      label: widget.semanticLabel ?? ' ',
      child: result,
    );
  }

  if(widget.frameBuilder ! =null) result = widget.frameBuilder! (context, result, _frameNumber, _wasSynchronouslyLoaded);if(widget.loadingBuilder ! =null) result = widget.loadingBuilder! (context, result, _loadingProgress);return result;
}
Copy the code

RawImage

Image control is only responsible for Image acquisition and logical processing, the real place to draw the Image is RawImage.

RawImage inherits from LeafRenderObjectWidget.

class RawImage extends LeafRenderObjectWidget
Copy the code

Render components using RenderImage.

  RenderImage createRenderObject(BuildContext context) {
    assert((! matchTextDirection && alignmentis Alignment) || debugCheckHasDirectionality(context));
    assert( image? .debugGetOpenHandleStackTraces()?.isNotEmpty ??true.'Creator of a RawImage disposed of the image when the RawImage still '
      'needed it.',);returnRenderImage( image: image? .clone(), debugImageLabel: debugImageLabel, width: width, height: height, scale: scale, color: color, opacity: opacity, colorBlendMode: colorBlendMode, fit: fit, alignment: alignment,///.
    );
  }
Copy the code

RenderImage inherits from RenderBox, so it needs to provide its own Size, as specified in performLayout.

@override
void performLayout() {
  size = _sizeForConstraints(constraints);
}
Copy the code
Size _sizeForConstraints(BoxConstraints constraints) {
  // Folds the given |width| and |height| into |constraints| so they can all
  // be treated uniformly.
  constraints = BoxConstraints.tightFor(
    width: _width,
    height: _height,
  ).enforce(constraints);

  if (_image == null)
    return constraints.smallest;

  returnconstraints.constrainSizeAndAttemptToPreserveAspectRatio(Size( _image! .width.toDouble() / _scale, _image! .height.toDouble() / _scale, )); }Copy the code

The constraint Size returned by _image == null is smalleset (0,0).

The rendering logic for RenderImage is in the paint method.

@override
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, opacity: _opacity? .value ??1.0, colorFilter: _colorFilter, fit: _fit, alignment: _resolvedAlignment! , centerSlice: _centerSlice, repeat: _repeat, flipHorizontally: _flipHorizontally! , invertColors: invertColors, filterQuality: _filterQuality, isAntiAlias: _isAntiAlias, ); }Copy the code

Finally, the actual drawing is done with paintImage.

ImageCache

Dart ImageCache is initialized in the binding.dart file, and the ImageCache is initialized in the binding.dart file.

mixin PaintingBinding on BindingBase, ServicesBinding {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    ///Initialize the image cache
    _imageCache = createImageCache();
    shaderWarmUp?.execute();
  }
}  
Copy the code

We can replace the global ImageCache by inheritance, but we generally don’t need to.

class ImageCache {
  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
  final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{};
Copy the code

There are three caches for ImageCache:

  • _liveImage

    Live LiveImage cache is used to ensure the flow, creating time will create a ImageStreamCompleterHandler, when no other Listener flow will release ImageStreamCompleterHandler, And removed from the cache map.

    If the loaded image is not cached, it will be loaded by loader, and then _trackLiveImage will be called and stored in the cache.

    try {
      result = loader();
      _trackLiveImage(key, result, null);
    } catch (error, stackTrace) {
      //....
    }
    
    Copy the code
    void _trackLiveImage(Object key, ImageStreamCompleter completer, int? sizeBytes) {
      // Avoid adding unnecessary callbacks to the finisher
      _liveImages.putIfAbsent(key, () {
        // Even if the caller of imageProvider.resolve is not listening for the stream, the cache will listen for the stream, and it will delete itself once the image is finished moving it from pending to keepAlive. Even if the cache size is 0, we still add this tracker, which will add a keepalive handle to the stream.
        return_LiveImage( completer, () { _liveImages.remove(key); }); }).sizeBytes ?? = sizeBytes; }Copy the code

    _LiveImage:

    class _LiveImage extends _CachedImageBase {
      _LiveImage(ImageStreamCompleter completer, VoidCallback handleRemove, {int? sizeBytes})
          / / will create ImageStreamCompleterHandler parent
          : 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
  • CacheImage

    This CacheImage is used to record an image stream that has been loaded. When the image is loaded, the _touch method is called to add it to the cache.

    class _CachedImage extends _CachedImageBase {
      _CachedImage(ImageStreamCompleter completer, {int? sizeBytes})
          // This Cache contains a stream of images that have already been loaded
          : super(completer, sizeBytes: sizeBytes);
    }
    Copy the code
  • PendingImage

    This cache is used to record 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 _LiveImage and CacheImage

abstract class _CachedImageBase {
  _CachedImageBase(
    this.completer, {
    this.sizeBytes,
  }) : assert(completer ! =null),
       // Create ImageStreamCompleter to ensure that the stream is not disposed
       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

In the constructor creates ImageStreamCompleterHandler, when the dispose for release.

Cache optimization

ImageCache provides the maximum ImageCache (1000 images by default), maximum memory usage (100MB by default), and basic putIfAbsent, evict, and clear methods.

If Image memory needs to be reduced, we can clean the cache in ImageCache as required. For example, when dispose Image in the list is disposed, we can try to remove its cache 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

Generally, ImageCache uses the return value of the _imagepprovider. obtainKey method as the key. When the ImageCache needs to be removed, we retrieve the cached key and remove it from ImageCache.

Note that unfinished image caches cannot be cleared. This is because the implementation class of ImageStreamCompleter listens for the asynchronously loaded event stream, 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 is disposed.

Clearing the memory cache is a way to swap time for space, and image presentation will require additional loading and decoding time. We need to use it carefully.

Optimization idea

  • Change the size of the cache

    // Change the maximum cache size
    const int _kDefaultSize = 100;
    const int _kDefaultSizeBytes = 50 << 20;  
    Copy the code
  • Reduce the size of images in memory

    In Android, you can use BitmapFactory to load raw width and height data before loading images into memory, and then reduce the memory footprint by reducing the sampling rate

    This idea is also possible with Flutter. Before the original Image is decoded into an Image, it can be assigned an appropriate size, which can significantly reduce the memory footprint.

    In fact, the official has provided us with a ResizeImage to reduce the decoded Image, but we need to specify the width or height of the cache for the Image in advance. If specified, the image will be scaled.

    The realization principle of ResizeImage is not complicated. It is equivalent to an agent. When loading images, it will agent the original loading operation as follows:

    Image.network(
      / /...
      Map<String.String>? headers,
      int? cacheWidth,
      int? cacheHeight,
    }) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
         assert(alignment ! =null),
         assert(repeat ! =null),
         assert(matchTextDirection ! =null),
         assert(cacheWidth == null || cacheWidth > 0),
         assert(cacheHeight == null || cacheHeight > 0),
         assert(isAntiAlias ! =null),
         super(key: key);
    Copy the code
    static ImageProvider<Object> resizeIfNeeded(int? cacheWidth, int? cacheHeight, ImageProvider<Object> provider) {
      if(cacheWidth ! =null|| cacheHeight ! =null) {
        return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
      }
      return provider;
    }
    Copy the code
    @override
    ImageStreamCompleter load(ResizeImageKey key, DecoderCallback decode) {
      Future<ui.Codec> decodeResize(Uint8List bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
        assert(
          cacheWidth == null && cacheHeight == null && allowUpscaling == null.'ResizeImage cannot be composed with another ImageProvider that applies '
          'cacheWidth, cacheHeight, or allowUpscaling.',);return decode(bytes, cacheWidth: width, cacheHeight: height, allowUpscaling: this.allowUpscaling);
      }
      final ImageStreamCompleter completer = imageProvider.load(key._providerCacheKey, decodeResize);
      if(! kReleaseMode) { completer.debugLabel ='${completer.debugLabel} - Resized(${key._width}x${key._height}) ';
      }
      return completer;
    }
    Copy the code

    As shown in the code above, when the NetworkImage is loaded, the resizeIfNeeded method is called, in which the ResizeImage will be returned if the cache width is used, otherwise the NetworkImage will be returned directly.

    If the cache width is used, the image is loaded in the load method above, which is decorated for decode and passes in the cache width. Finally, we call the imageProvider(NetworkImage) to load the image, and finally set the cache size for us.

    The source of deocde is PaintingBinding. Instance! InstantiateImageCodec. The concrete implementation is as follows:

    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);
      return ui.instantiateImageCodec(
        bytes,
        targetWidth: cacheWidth,
        targetHeight: cacheHeight,
        allowUpscaling: allowUpscaling,
      );
    }
    Copy the code
    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;
        }
      }
      buffer.dispose();
      ////Specify the desired width and height
      return descriptor.instantiateCodec(
        targetWidth: targetWidth,
        targetHeight: targetHeight,
      );
    }
    Copy the code

    We can see that the cache width ultimately affects the targetWidth and targetHeight attributes.

    By now we should know how to optimize the memory size by limiting the size, but it is quite troublesome to change the size of the cache each time the image is loaded. Here we recommend a large autu_resize_image, which is easier to use. If necessary, please refer to it

  • Increasing disk cache

    Future<ui.Codec> _loadAsync(NetworkImage key,StreamController<ImageChunkEvent> chunkEvents, image_provider.DecoderCallback decode,) async {
        try {
          assert(key == this);
       //-------- Add code 1 begin--------------
       // Check whether there is a local cache
        final Uint8List cacheImageBytes = await ImageCacheUtil.getImageBytes(key.url);
        if(cacheImageBytes ! =null) {
          return decode(cacheImageBytes);
        }
       //-------- add code 1 end--------------
    
        / /... omit
          if (bytes.lengthInBytes == 0)
            throw Exception('NetworkImage is an empty file: $resolved');
    
            //-------- Add code 2 begin--------------
           // To cache image data locally, you need to customize a specific cache policy
           await ImageCacheUtil.saveImageBytesToLocal(key.url, bytes);
           //-------- add code 2 end--------------
    
          return decode(bytes);
        } finally{ chunkEvents.close(); }}Copy the code

    You just need to improve the way you load images. However, this is not recommended because it invades Image’s source code.

  • Clearing the memory cache

     PaintingBinding.instance.imageCache.clear();
    Copy the code

    This way can be handled according to their own needs, nothing more than the way of time for space, after using, loading pictures will be re-downloaded decoding.

  • Use third-party libraries

    Flutter_cached_network_image – this library implements local image caching, if necessary.

Write in the last

At this point, the whole article is finished. To be honest, at the beginning, I also had a little understanding. Finally, I learned and understood through checking materials and blogs, and then sorted out the whole process, and finally wrote an article, which is convenient for myself to recall and others to understand.

If this article is helpful to your place, we are honored, if there are mistakes and questions in the article, welcome to put forward!

The resources

Optimization of Flutter image loading

Loading of Flutter picture

Omit…

Recommended reading

  • This article learns about BuildContext
  • The principle and use of Key
  • Construction process analysis of Flutter three trees
  • Start, render, setState process
  • Flutter layout process
  • Android integration Flutter | and interaction