Flutter development - image loading and cache source code analysis

There is a picture component in Flutter: Image, usually use its Image.network(src), Image.file(src), Image.asset(src)to load pictures.
Here's Imagethe general constructor for :

  const Image({
    
    
    super.key,
    required this.image,
    this.frameBuilder,
    this.loadingBuilder,
    this.errorBuilder,
    this.semanticLabel,
    this.excludeFromSemantics = false,
    this.width,
    this.height,
    this.color,
    this.opacity,
    this.colorBlendMode,
    this.fit,
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.centerSlice,
    this.matchTextDirection = false,
    this.gaplessPlayback = false,
    this.isAntiAlias = false,
    this.filterQuality = FilterQuality.low,
  })

It can be seen from its construction method that the Image component has a mandatory parameter image, which is of type ImageProvider. ImageProvider is an abstract class that defines related interfaces for image data acquisition and loading. It has two main responsibilities:

  • 1. Provide image data source;
  • 2. Cache pictures;

ImageProviderAbstract class:

abstract class ImageProvider<T extends Object> {
    
    
  const ImageProvider();
  
  ImageStream resolve(ImageConfiguration configuration) {
    
    
    ...
  }
  
  ImageStream createStream(ImageConfiguration configuration) {
    
    
    return ImageStream();
  }

  
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
    
    
    ...
  }
  
  Future<bool> evict({
    
     ImageCache? cache, ImageConfiguration configuration = ImageConfiguration.empty }) async {
    
    
    ...
  }
  
  Future<T> obtainKey(ImageConfiguration configuration);
}

From the source code above, it can be found that the loading and parsing of images is done ImageProviderby , specifically by its subclasses. ImageProviderMany subclasses are derived, such as NetworkImageclass and AssetImageclass, NetworkImagewhich load image data from the network and AssetImageload from resource files in the installation package.

Image loading

A method is provided in ImageProvider load(), which is an interface for loading image data sources, and different data sources are loaded in different ways.
Loading network images is used Image.network(), and the corresponding ImageProvider is NetworkImagea class, which implements the load() method:

  
  ImageStreamCompleter load(FileImage key, DecoderCallback decode) {
    
    
    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, null, decode),
      scale: key.scale,
      debugLabel: key.file.path,
      informationCollector: () => <DiagnosticsNode>[
        ErrorDescription('Path: ${
      
      file.path}'),
      ],
    );
  }
  • The return value type of the load method is ImageStreamCompleter, which is an abstract class that defines some interfaces for managing the image loading process. Image Widget uses it to monitor the image loading status;
  • MultiFrameImageStreamCompleter is a subclass of ImageStreamCompleter. Implementing this class can quickly create an ImageStreamCompleter instance;

MultiFrameImageSteamCompleter has a codec parameter, which is used to call _loadAsync()the method in the source code. The implementation of the method is as follows:

Future<ui.Codec> _loadAsync(
   NetworkImage key,
   StreamController<ImageChunkEvent> chunkEvents,
   image_provider.DecoderBufferCallback? decode,
   image_provider.DecoderCallback? decodeDepreacted,
   ) async {
    
    
 try {
    
    
   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>>(<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 (decode != null) {
    
    
       final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
       return decode(buffer);
     } else {
    
    
       assert(decodeDepreacted != null);
       return decodeDepreacted!(bytes);
     }
}

Through the source code, it can be found _loadAsync()that the method is responsible for downloading the picture, and calls decode()the method to decode the downloaded picture data.

image cache

The key method of image caching is: obtainKey(ImageConfiguration)
This method is mainly to cooperate with the realization of image caching. After ImageProvider loads data from the data source, it will cache the image data in the global ImageCache, and the image data cache is a Map, and the key of the Map is It is the return value of calling this method, and different keys represent different image data caches.
resolveThe method is ImageProviderthe main entry method exposed to Image, which receives an ImageConfiguration parameter and returns an ImageStream.

ImageStream resolve(ImageConfiguration configuration) {
    
    
  assert(configuration != null);
  final ImageStream stream = createStream(configuration);
  _createErrorHandlerAndKey(
    configuration,
        (T key, ImageErrorListener errorHandler) {
    
    
      resolveStreamForKey(configuration, stream, key, errorHandler);
    },
       ...
  );
  return stream;
}

ImageConfiguration: Contains information about images and devices. Called internally _createErrorHandlerAndKeyto load the key and create an error handler.
resolveStreamForKeymethod:


void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
    
    
  if (stream.completer != null) {
    
    
    //缓存逻辑
    final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent(
      key,
          () => stream.completer!,
      onError: handleError,
    );
    assert(identical(completer, stream.completer));
    return;
  }
  //缓存逻辑
  final ImageStreamCompleter? completer = PaintingBinding.instance.imageCache.putIfAbsent(
    key,
        () => loadBuffer(key, PaintingBinding.instance.instantiateImageCodecFromBuffer),
    onError: handleError,
  );
  if (completer != null) {
    
    
    stream.setCompleter(completer);
  }
}

In the resolve method, resolveStreamForKey is called, which PaintingBinding.instance.imageCacheis an instance of ImageCache, which is a property of PaintingBinding, and both PaintingBinding.instance and imageCache are singletons, so the image cache is global and managed PaintingBinding.instance.imageCacheuniformly

The specific implementation of ImageCache caching

Definition of ImageCache:

const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
class ImageCache {
    
    
  final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{
    
    };
  final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{
    
    };

  final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{
    
    };

  int get maximumSize => _maximumSize;
  int _maximumSize = _kDefaultSize;
  ...
}

There are three cache pools in ImageCache:

  • _pendingImages: Used to store the images being loaded and decoded. When the images are loaded and decoded, ImageCache will automatically remove the corresponding Entry of _pendingImages;
  • _cache: Used to store all loaded images. If the number of image caches and the memory usage size do not exceed the upper limit of ImageCache, _cache will always keep the Cache Entry, and if it exceeds, it will be released according to LRU;
  • _liveImages: Used to store images in use. When Image Widget removes or replaces images, or Image Widget itself is removed, ImageCache will remove the corresponding Entry from _liveImages; only ImageCache releases the same image from all cache
    pools Entry, the picture is actually released in memory.

How to Generate the Key of Image Cache

Because the value of the same key in the Map will be overwritten, that is to say, the key is the unique identifier of the image cache. As long as the keys are different, the image data will be distributed in the cache. So what is the unique identifier of the picture? You can see the method from the source code ImageProvider.obtainKey(). The key used by the image cache is generated by this method, and the subclass of ImageProvider rewrites this method.
insert image description here
This means that different ImageProviders define keys differently. The obtainKey() method of NetworkImage:

  
  Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
    
    
    return SynchronousFuture<NetworkImage>(this);
  }

Create a SynchronousFuture, and then return itself, so when comparing keys, ==just look at the operator:

  
  bool operator ==(Object other) {
    
    
    if (other.runtimeType != runtimeType) {
    
    
      return false;
    }
    return other is NetworkImage && other.url == url && other.scale == scale;
  }

For network pictures, the key is actually url+scale. So the url and scale of the two pictures are the same, so they will only cache one copy in memory.

set cache size

There is a default cache size in the ImageCache class:

const int _kDefaultSize = 1000;//最多1000张
const int _kDefaultSizeBytes = 100 << 20; //最大 100 MiB

We can also set a custom cache limit through the following code:

 PaintingBinding.instance.imageCache.maximumSize=500; //最多500张
 PaintingBinding.instance.imageCache.maximumSizeBytes = 50 << 20; //最大50M

Guess you like

Origin blog.csdn.net/Memory_of_the_wind/article/details/131350227