Learn today how Flutter itself loads and manages images. Flutter provides an Image control, Image, that defines several ways to load images, including image. asset, image. file, Image.network, and image.memory. The Image maintains an ImageProvider object inside the Image, and the ImageProvider does the actual work of loading the Image. The Widget itself is internally represented in the RawImage:

The image control

// ImageWidget result = RawImage( image: _imageInfo? .image, debugImageLabel: _imageInfo? .debugLabel, width: widget.width, height: widget.height, scale: _imageInfo?.scale ??1.0,
      color: widget.color,
      colorBlendMode: widget.colorBlendMode,
      fit: widget.fit,
      alignment: widget.alignment,
      repeat: widget.repeat,
      centerSlice: widget.centerSlice,
      matchTextDirection: widget.matchTextDirection,
      invertColors: _invertColors,
      isAntiAlias: widget.isAntiAlias,
      filterQuality: widget.filterQuality,
    );
return result;
Copy the code

Here you can see that _imageInfo determines how RawImage displays the image. _imageInfo reassigns each frame of the image:

// image.dart
void _handleImageFrame(ImageInfo imageInfo, boolsynchronousCall) { setState(() { _imageInfo = imageInfo; }}Copy the code

So where does the image information come from? It’s generated by the _resolveImage method. This method is called in the didChangeDependencies, didUpdateWidget, and ReAssemble methods of _ImageState. That is, when the control changes to refresh the state, it will re-parse the image.

Image resolution

The _resolveImage logic is as follows:

void _resolveImage() {
	final ScrollAwareImageProvider provider = ScrollAwareImageProvider<dynamic>(
      context: _scrollAwareContext,
      imageProvider: widget.image,
    );
	finalImageStream newStream = provider.resolve(createLocalImageConfiguration( context, size: widget.width ! =null&& widget.height ! =null ? Size(widget.width, widget.height) : null)); _updateSourceStream(newStream); }Copy the code

I’m going to wrap it up with a ScrollAwareImageProvider, which we’ll talk about later, but I’m going to skip this.

//ImageProvider# resolve
ImageStream resolve(ImageConfiguration configuration) {
	_createErrorHandlerAndKey(configuration,(T key, ImageErrorListener errorHandler) {
        resolveStreamForKey(configuration, stream, key, errorHandler);
      },
		(T? key, dynamic exception, StackTrace? stack) async {
        await null; // wait an event turn in case a listener has been added to the image stream.
        final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
        stream.setCompleter(imageCompleter);
        InformationCollector? collector;
        assert(() {
          collector = () sync* {
            yield DiagnosticsProperty<ImageProvider>('Image provider'.this);
            yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
            yield DiagnosticsProperty<T>('Image key', key, defaultValue: null);
          };
          return true; } ()); imageCompleter.setError( exception: exception, stack: stack, context: ErrorDescription('while resolving an image'),
          silent: true.// could be a network error or whatnotinformationCollector: collector, ); }); }Copy the code

The resolve method calls _createErrorHandlerAndKey to handle image loading exceptions. When the image loads properly, resolveStreamForKey is executed.

//resolveStreamForKey
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, );return;
	}

	finalImageStreamCompleter? completer = PaintingBinding.instance! .imageCache! .putIfAbsent( key, () => load(key, PaintingBinding.instance! .instantiateImageCodec), onError: handleError, );if(completer ! =null) { stream.setCompleter(completer); }}Copy the code

Flutter maintains image caching logic in the ImageCache object.

Cache management

ImageCacheThere are three maps:

respectively

  • The image being loaded
  • Image cached in memory
  • Represents an active image that may be cleared when the Widget status changes

The new cache

When adding a cache, the map key is set. The key is provided by the ImageProvider object. Such as:

  • AssetImage The key is considered the same when the package name is the same as the bundle.
  • NetworkImage The key is considered the same when the image URL and scale are the same.

ImageCache is actually a singleton object. That is, Flutter image cache management is global. The most important method for ImageCache is putIfAbsent:

// Clean the core logic of the code
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener? onError }) {
  // Retrieve the cache from the loaded map based on the key, if there is a direct returnImageStreamCompleter? result = _pendingImages[key]? .completer;if(result ! =null) {
      return result;
    }
  
  // Check the memory cache and update the map if it exists
  final _CachedImage? image = _cache.remove(key);
  if(image ! =null) {
    _trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
    _cache[key] = image;
    return image.completer;
  }
  
  // No cache, fetch from _live
  final _CachedImage? liveImage = _liveImages[key];
  if(liveImage ! =null) {
    // Update the cache
    _touch(key, liveImage, timelineTask);
    return liveImage.completer;
  }
  
  // None of the three maps can fetch cached images
  result = loader(); / / load
  _trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
  
	_PendingImage? untrackedPendingImage;

  // Define a listener
	void listener(ImageInfo? info, bool syncCall) {
		// Load the listener
	}
  
  // Wrap a listener
	final ImageStreamListener streamListener = ImageStreamListener(listener);
	if (maximumSize > 0 && maximumSizeBytes > 0) {
		// Put it in the cache
		_pendingImages[key] = _PendingImage(result, streamListener);
	} else {
		untrackedPendingImage = _PendingImage(result, streamListener);
	}
	// Add a listener
	result.addListener(streamListener);
	return result;
}
Copy the code

When the state of the Image changes, the liveImages will be modified:

// Image
_imageStream.removeListener(_getListener());

// ImageStream
void removeListener(ImageStreamListener listener) {
  for (final VoidCallback callback in _onLastListenerRemovedCallbacks) {
    callback();
  }
  _onLastListenerRemovedCallbacks.clear();
}
Copy the code

Callback = trackLiveImage; callback = trackLiveImage;

_trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
Copy the code

The modified image will be removed from _liveImages.

It can be seen that the priority of cache is pending -> cache -> live -> Load. The process of image cache and acquisition is as follows:

Cache clearing

When updating the cache size, we also check the cache size:

void _checkCacheSize(TimelineTask? timelineTask) {
  while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
    final Object key = _cache.keys.first;
    final_CachedImage image = _cache[key]! ; _currentSizeBytes -= image.sizeBytes! ; _cache.remove(key); }}Copy the code

When the current total cache capacity is greater than the maximum capacity or the number of caches is greater than the maximum number, the cache is cleared. So when using the cache above, the cache that is accessed multiple times will put the key back to avoid being cleaned up immediately. So Flutter’s own cache cleaning algorithm follows the “least recently used” approach. The logic for image caching is shown below:

Image to load

Image loading mainly relies on the load method above. Different ImageProvider subclasses have their own implementations. For example,

  • AssetImage
return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, decode),
      scale: key.scale,
      debugLabel: key.name,
      informationCollector: collector
    );
Copy the code
  • NetworkImage
final StreamController<ImageChunkEvent> chunkEvents =
        StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
        chunkEvents: chunkEvents.stream,
        codec: _loadAsync(key as NetworkImage, decode, chunkEvents),
        scale: key.scale,
        debugLabel: key.url,
        informationCollector: _imageStreamInformationCollector(key));
Copy the code

The logic is basically the same, and the specific flow is embodied in loadAsync:

// AssetImage _loadAsync
try {
      data = await key.bundle.load(key.name);
    } onFlutterError { PaintingBinding.instance! .imageCache! .evict(key);rethrow;
    }

if (data == null) {
// Load data is null, clear the cache for this keyPaintingBinding.instance! .imageCache! .evict(key);throw StateError('Unable to read data');
}

return await decode(data.buffer.asUint8List());


/// NetworkImage _loadAsync
Future<ui.Codec> _loadAsync(
      NetworkImage key,
      image_provider.DecoderCallback decode,
      StreamController<ImageChunkEvent> chunkEvents) {

	final Uri resolved = Uri.base.resolve(key.url);
	return ui.webOnlyInstantiateImageCodecFromUrl(resolved, // ignore: undefined_function
        chunkCallback: (int bytes, int total) {
      chunkEvents.add(ImageChunkEvent(
          cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
    }) as Future<ui.Codec>;
}
Copy the code

This will load images from the bundle and pull images from the network.

Sliding process

Remember the ScrollAwareImageProvider mentioned above, where there is a judgment about sliding:

if(Scrollable.recommendDeferredLoadingForContext(context.context)) { SchedulerBinding.instance.scheduleFrameCallback((_) {  scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError)); });return;
}
Copy the code

When the logic in the if holds, the task of parsing the image is moved to the next frame. RecommendDeferredLoadingForContext specific logic:

static bool recommendDeferredLoadingForContext(BuildContext context) {
	
	final_ScrollableScope widget = context.getElementForInheritedWidgetOfExactType<_ScrollableScope>()? .widgetas _ScrollableScope;
	if (widget == null) {
      return false;
    }
	// There are sliding widgets
	return widget.position.recommendDeferredLoading(context);
}
Copy the code

This will find the nearest _ScrollableScope in the Widget tree. Returns true if the ScrollableScope is on a fast slide. So Flutter does not load images in a fast sliding list.

conclusion

This concludes the load and cache management of Flutter images. We can recognize several problems

  • Flutter itself is an in-memory cache of images. Also according to LRU algorithm to manage the cache. And the cache pool has a threshold, so we can set the memory threshold we want.
  • Flutter itself does not provide a disk cache of images. The image loading process will restart after the APP restarts.