Fluent | image source code analysis and optimization method

preface

Image is a small component used by fluent to display images. It can load images in network, local, file or memory, and supports JPEG, PNG, GIF, animated GIF, WebP, animated WebP, BMP and WBMP formats. Fluent image itself also implements the mechanism of memory cache, which can greatly improve the speed of image display.

Review how Image is opened

  • Image.network
Image.network("Picture address",fit: BoxFit.cover,width: ,height: 400)
 
  • Image.file
Image.file(File("Local picture path"));
 
  • Image.asset
Image.memory(Uint8List.fromList([]));
 
  • Image.memory
Image.memory(Uint8List.fromList([]));

A byte array needs to be passed in

The resolution of the Image loaded by the shutter

Fluent can load appropriate resolution pictures for the current device, and specify the picture allocation of different resolutions, as shown in the following figure:

The main resource corresponds to the resolution of 1.0x by default. If it is greater than 1.0, the image file under 2.0x will be selected. Pictures in fluent must be declared in pubspec Yaml file, as shown in the following figure:

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

pubspec. Every picture of what is in yaml file should correspond to the actual file. Accordingly, when the main resource image is missing, it will be loaded from the highest order according to the resolution.

When fluent packages applications, resources will be stored in apk's assets / fluent in the form of key value_ assets/AssetManifest. In the Josn file, the file will be parsed when loading resources, and the most appropriate file will be selected for loading and display. The details are as follows:

Flutter.network source code analysis

Before you start, just look at some classes. When the whole process is over, it will be better to look back:

  • Image: used to display pictures
  • _ Imagestate: the state class of image, which handles the life cycle and calls loading.
  • ImageProvider: image provider, used to load images, such as NetWrokImage, ResizeImage, etc.
  • ImageStreamCompleter: image resource management class
  • ImageStream: ImageStream is a handle to a picture resource, which holds the picture resource, the callback after loading and the picture resource manager. The ImageStreamCompleter object is the management class of image resources
  • MultiFrameImageStreamCompleter: multiframe image parser
  • ImageStreamListener: listen for the loading result of the picture, call back after loading, and then refresh the page to display the picture

Let's start the whole process:

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

Using image When network creates an image object, the instance variable image is initialized.

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

image is actually the provider of ImageProvider pictures. It is an abstract class with the following subclasses:

The NetWorkImage class is used for network image loading.

Let's look at the Image component directly. The Image component itself is a StatefulWidget, and its own State is determined by_ ImageState to manage. According to the life cycle of State, we can know that the initState method is executed first

initState

@override
void initState() {
  super.initState();
  //Add monitoring of system settings, such as screen rotation, etc  
  WidgetsBinding.instance!.addObserver(this);
  //Provide non disclosure access to BuildContext
  _scrollAwareContext = DisposableBuildContext<State<Image>>(this);
}
Copy code

didChangeDependencies

After initState() is executed, didChangeDependencies() will be executed as follows:

@override
void didChangeDependencies() {
  _updateInvertColors();
  _resolveImage();///Parse picture

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

  super.didChangeDependencies();
}
Copy code
ImageState._resolveImage
void _resolveImage() {
  //ScrollAwareImageProvider can avoid loading pictures when scrolling
  final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
    context: _scrollAwareContext,
    imageProvider: widget.image,
  );
  ///Create ImageStream object  
  final ImageStream newStream =
    //The 'resolve' method of 'ImageProvder' is called here. As follows:
    provider.resolve(createLocalImageConfiguration(
      context,
      size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
    ));
  assert(newStream != null);
  ///Update flow  
  _updateSourceStream(newStream);
}
Copy 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) {
      ///Attempt to set ImageStreamCompleter for Stream  
      resolveStreamForKey(configuration, stream, key, errorHandler);
    },
    (T? key, Object exception, StackTrace? stack) async {
     
    },
  );
  return stream;
}
ImageStream createStream(ImageConfiguration configuration) {
   return ImageStream();
}
Copy code

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

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

resolveStreamForKey
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
  ///If it is not empty, it indicates that the flow has been completed and is directly stored in the cache here
  if (stream.completer != null) {
    ///Store in cache  
    final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
      key,
      () => stream.completer!,
      onError: handleError,
    );
    assert(identical(completer, stream.completer));
    return;
  }
  ///If the stream is not completed, it is stored in the cache, and the load method of loading pictures is also passed into it.
  final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
    key,
    ///This closure calls imageprovider Load method
    ///Note that the second parameter of 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 code

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

_ ImageState passed

ImageCache.putIfAbsent
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {

  ImageStreamCompleter? result = _pendingImages[key]?.completer;
  // If it is the first loading, result == null
  if (result != null) {
    return result;
  }

  // If it is the first time to load, here image == null  
  final _CachedImage? image = _cache.remove(key);
  if (image != null) {
    // Ensure that this ImageStream is alive and stored in 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 time to load, liveImage == null  
  if (liveImage != null) {
    // This_ The stream of LiveImage may have been completed. The specific condition is that sizeBytes is not empty
    // If not, it will be released_ aliveHandler created by CachedImage  
    _touch(
      key,
      _CachedImage(
        liveImage.completer,
        sizeBytes: liveImage.sizeBytes,
      ),
      timelineTask,
    );
    return liveImage.completer;
  }

  try {
    // If there is no in the cache, imageprovider is called Load method
    result = loader();
    // Ensure that the stream is not dispose d  
    _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;
      // Every Listener will cause imageinfo The image reference count is + 1. If it is not released, the image cannot be released.
      // Release this_ Image processing  
      info.dispose();
    }
    // Active count + 1  
    final _CachedImage image = _CachedImage(
      result!,
      sizeBytes: sizeBytes,
    );
	// Active count + 1, may also be ignored
    _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 pictures directly  
      image.dispose();
    }

    final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
    if (pendingImage != null) {
      ///Remove the picture listening in loading. If it is the last one, then_ LiveImage will also be released  
      pendingImage.removeListener();
    }
    listenedOnce = true;
  }

  final ImageStreamListener streamListener = ImageStreamListener(listener);
  if (maximumSize > 0 && maximumSizeBytes > 0) {
    ///map stored in loading  
    _pendingImages[key] = _PendingImage(result, streamListener);
  } else {
    ///If the cache is not set, a field save will also be called to prevent the previous save_ Memory leak caused by LiveImage   
    untrackedPendingImage = _PendingImage(result, streamListener);
  }
  //  ImageProvider. Register and listen to the completer returned by the load method
  result.addListener(streamListener);
 
  return result;
}
Copy code

Try to put the request into the global cache ImageCache and set listening

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 code

The load method is an abstract method. The load method here is the implementation class NetWorkImage. This code creates a MultiFrameImageStreamCompleter object and returns it. This is a multiframe image manager, indicating that Fluter supports GIF images. The codec variable that creates the object is_ Initialize with 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 code

This method is the operation of downloading image source data. Different data sources have different logic. After downloading, instantiate the image decoder object Codec according to the binary data of the picture, and then return. Next, let's look at the 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 code

The asynchronous method of codec will be called after execution_ The handleCodecReady function saves the codec object in the method, and then calls the decodeNextFrameAndSchedule decodes picture frames.

If the picture is not in animation format, execute_ The emitFrame function takes the picture frame object from the frame data, creates an ImageInfo object according to the scaling scale, and then sets the picture information

void _emitFrame(ImageInfo imageInfo) {
  setImage(imageInfo);
  _framesEmitted += 1;
}
  @protected
  @pragma('vm:notify-debugger-on-exception')
  void setImage(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 code

At this time, a new picture needs to be rendered according to the listener. When was this listener added? Let's go back to the didChangeDependencies method and finish the execution_ The resolveImage method is executed after_ listenToStream method.

ImageState.__updateSourceStream

Will_ Update imageStream to newStream and move the stream listener registration from the old stream to the new stream if the listener is already registered.

newStream is_ ImageStream created in resolveImage() method.

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

  if (_isListeningToStream)///The initial value is false
    _imageStream!.removeListener(_getListener());

  if (!widget.gaplessPlayback)// When the ImageProvider changes the incident, it also displays the old picture. The default is true
    setState(() { _replaceImage(info: null); }); // Leave ImageInfo blank

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

  _imageStream = newStream; // Save current ImageStream
  if (_isListeningToStream) ///The initial value is false
    _imageStream!.addListener(_getListener());
}
Copy code
ImageState._listenToStream

In the MultiFrameImageStreamCompleter class above, after the image is processed, it is encapsulated into an ImageInfo object, and then it will be notified through the added listener.

The listener is through_ Added by the listenToStream method_ The listenToStream method will be in the didChangeDependencies method_ The resolveImage method is executed after execution. The details are as follows:

void _listenToStream() {
  if (_isListeningToStream) //The initial value is false
    return;
  ///Add listening to the stream. The ImageInfo of each listening is the clone in the Completer
  _imageStream!.addListener(_getListener());
  _completerHandle?.dispose();
  _completerHandle = null;

  _isListeningToStream = true;
}
Copy code

The method is directed to_ A listener is added to the imageStream object, which passes_ getListener.

ImageState._getListener
ImageStreamListener _getListener({bool recreateListener = false}) {
  if(_imageStreamListener == null || recreateListener) {
    _lastException = null;
    _lastStack = null;
    ///Create ImageStreamListener  
    _imageStreamListener = ImageStreamListener(
      ///Process ImageInfo callback  
      _handleImageFrame,
      ///Byte stream callback  
      onChunk: widget.loadingBuilder == null ? null : _handleImageChunk,
      ///Error callback  
      onError: 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 code

Create a Listener for ImageStream.

ImageState._handleImageFrame

The part of the Listener that handles the ImageInfo callback. When there is a new need to render, the listening method will be called. Finally, the setState() method will be called to notify the interface to 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 code

build

Widget build(BuildContext context) {
  if (_lastException != null) {
    if (widget.errorBuilder != null)
      return widget.errorBuilder!(context, _lastException!, _lastStack);
    if (kDebugMode)
      return _debugBuildErrorWidget(context, _lastException!);
  }
  // Display using RawImage_ imageInfo?.image. If image is empty, the size of RawImage is Size(0,0)
  // If the loading is completed, 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 picture data
    debugImageLabel: _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 code

RawImage

In fact, the Image control is only responsible for the acquisition and logical processing of pictures. The real place to draw pictures is RawImage.

RawImage inherits from LeafRenderObjectWidget.

class RawImage extends LeafRenderObjectWidget
 Copy code

Render components through RenderImage.

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

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

@override
void performLayout() {
  size = _sizeForConstraints(constraints);
}
Copy 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;

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

You can see above_ When image == null, the returned constraint size is smallset, that is, Size(0,0).

The rendering logic of 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 code

Finally, the actual drawing is carried out through paintImage.

ImageCache

From the above, we know that the images loaded through the ImageProvider will have a cache in memory, which is a global image cache. The initialization of ImageCache is in binding In dart file:

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

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

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

There are three kinds of caches for ImageCache:

  • _liveImage LiveImage cache is used to ensure the survival of the stream. An ImageStreamCompleterHandler will be created when the stream is created. When the stream has no other Listener, the ImageStreamCompleterHandler will be released and removed from the cache map. When the loaded image is not cached, it will be loaded through the loader, and then called_ trackLiveImage is cached.
try {
  result = loader();
  _trackLiveImage(key, result, null);
} catch (error, stackTrace) {
  //....
}
void _trackLiveImage(Object key, ImageStreamCompleter completer, int? sizeBytes) {
  //Avoid adding unnecessary callbacks to the completer
  _liveImages.putIfAbsent(key, () {
    //Even imageprovider The caller of resolve does not listen to the stream, the cache listens to the stream, and once the image is moved from pending to keepAlive, it will delete itself. Even if the cache size is 0, we still add this tracker, which will add a keep alive handle to the stream.
    return _LiveImage(
      completer,
      () {
        _liveImages.remove(key);
      },
    );
  }).sizeBytes ??= sizeBytes;
}

_LiveImage:

class _LiveImage extends _CachedImageBase {
  _LiveImage(ImageStreamCompleter completer, VoidCallback handleRemove, {int? sizeBytes})
      //The parent class creates an ImageStreamCompleterHandler
      : super(completer, sizeBytes: sizeBytes) {
    _handleRemove = () {
      handleRemove();//Delete itself from the cached map
      dispose();
    };
    // Callback when Listener is empty      
    completer.addOnLastListenerRemovedCallback(_handleRemove);
  }

  late VoidCallback _handleRemove;

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

  @override
  String toString() => describeIdentity(this);
}
  • CacheImage This CacheImage is used to record the loaded picture stream. Called when the picture is loaded_ The touch method adds it to the cache,
class _CachedImage extends _CachedImageBase {
  _CachedImage(ImageStreamCompleter completer, {int? sizeBytes})
      //The loaded picture stream is added to this Cache
      : super(completer, sizeBytes: sizeBytes);
}

 
  • PendingImage This cache is used to record the picture stream in loading.
class _PendingImage {
  _PendingImage(this.completer, this.listener);

  final ImageStreamCompleter completer;
  final ImageStreamListener listener;

  void removeListener() {
    completer.removeListener(listener);
  }
}

 

_ Base classes for LiveImage and CacheImage

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

ImageStreamCompleterHandler will be created in the constructor and released when disposing.

Cache optimization

ImageCache provides the setting method of the maximum picture cache. The default number is 1000 pictures. At the same time, the maximum memory usage setting is 100MB by default. At the same time, there are basic putIfAbsent, evict and clear methods.

If we need to reduce the memory consumption of images, we can clean up the cache in ImageCache as required. For example, when the Image in the list is dispose d, 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 completed evict s
          _imagepProvider.evict();
        }
      },
    );
  }
  super.dispose();
}
Copy code

In general, ImageCache uses_ imagepProvider. The return value of the obtainkey method is used as the key. When the image cache needs to be removed, we get the cached key and remove it from the ImageCache.

It should be noted that the image cache that has not finished loading cannot be cleared. This is because the implementation class of ImageStreamCompleter listens to the event stream loaded asynchronously. When the asynchronous loading is completed, it will call the reportImageChunkEvent method, which will be called internally_ checkDisposed method. At this time, if the image is disposed, an exception will be thrown.

Clearing the memory cache is a way of changing time for space. Image display will require additional loading and decoding time. We need to use it carefully.

Optimization ideas

  • Modify cache size //Modify cache Max const int_ kDefaultSize = 100; const int _ kDefaultSizeBytes = 50 << 20; Copy code
  • Reduce the size of pictures in memory In Android, BitmapFactory can be used to load the original width and height data before loading the picture into memory, and then reduce the memory occupation by reducing the sampling rate In fluent, this idea is also feasible. Before the original Image is decoded into Image, we can assign it an appropriate size, which can significantly reduce the memory occupation. In fact, the official has provided us with a ResizeImage to reduce the decoded Image, but we need to specify the cache width or height for the Image in advance. If specified, the picture will be scaled. The implementation principle of ResizeImage is not complicated. It is equivalent to an agent. When loading pictures, it will act as an agent for 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);
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;
}
@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}×${key._height})';
  }
  return completer;
}

As shown in the above code, when loading the network image, the resizeifneed method will be called, in which it will be judged that if the cache width and height are used, the ResizeImage will be returned, otherwise the NetworkImage will be returned directly. If the cache width and height are used, when loading the picture, it will go to the load method above. The load method will make a layer of decoration for the decode, and the width and height of the incoming cache. Finally, load the image by calling the load of imageprovider (here it means NetworkImage), and finally decode it to set the cache size for us. The source of deocde is paintingbinding instance!. instantiateImageCodec. The specific 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,
  );
}
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,
  );
}

We can see that the cache width and height ultimately affect the targetWidth and targetHeight attributes. By now, we should know how to optimize the memory size by limiting the size, but it's troublesome to get a cache width and height every time you load pictures. Here we recommend autu written by a big man_ resize_ Image. It's easy to use. You can refer to it if necessary

  • Increase disk cache
Future<ui.Codec> _loadAsync(NetworkImage key,StreamController<ImageChunkEvent> chunkEvents, image_provider.DecoderCallback decode,) async {
    try {
      assert(key == this);
   //--------Add code 1 begin--------------
   // Determine whether there is a local cache
    final Uint8List cacheImageBytes = await ImageCacheUtil.getImageBytes(key.url);
    if(cacheImageBytes != null) {
      return decode(cacheImageBytes);
    }
   //--------Add code 1 end--------------

    //... ellipsis
      if (bytes.lengthInBytes == 0)
        throw Exception('NetworkImage is an empty file: $resolved');

        //--------New code 2 begin--------------
       // To cache image data locally, you need to customize specific caching strategies
       await ImageCacheUtil.saveImageBytesToLocal(key.url, bytes);
       //--------Add code 2 end--------------

      return decode(bytes);
    } finally {
      chunkEvents.close();
    }
  }

Just improve the method of loading pictures. However, this will invade the source code of Image and is not recommended.

  • Clean up memory cache
 PaintingBinding.instance.imageCache.clear();

This method can be handled according to their own needs, which is nothing more than the way of changing time and space. After use, the loaded pictures will be downloaded and decoded again.

  • Using third-party libraries flutter_cached_network_image , this library implements local image caching. You can learn about it if you need it.

Write at the end

Here, the whole article is over. To tell you the truth, at the beginning, you also have a little knowledge. Finally, you can learn and understand by viewing materials and blogs, sort out the whole process, and finally write an article to facilitate your own memory and others' understanding.

It's a great honor if this article can help you. If there are errors and questions in this article, you are welcome to put forward them!

reference material

Probe into the optimization of picture loading in fluent Fluent picture loading Omit

Added by Twentyoneth on Fri, 11 Feb 2022 11:52:44 +0200