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
ImageCache
There 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.