LCOV - code coverage report
Current view: top level - lib - image_cache_ext.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 58 194 29.9 %
Date: 2022-05-19 20:34:37 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright 2014 The Flutter Authors. All rights reserved.
       2             : // Use of this source code is governed by a BSD-style license that can be
       3             : // found in the LICENSE file.
       4             : 
       5             : import 'dart:async';
       6             : import 'dart:developer';
       7             : import 'dart:ui' show hashValues;
       8             : 
       9             : import 'package:flutter/cupertino.dart';
      10             : import 'package:flutter/foundation.dart';
      11             : import 'package:flutter/scheduler.dart';
      12             : import 'package:power_image_ext/remove_aware_map.dart';
      13             : 
      14             : import 'image_info_ext.dart';
      15             : import 'image_provider_ext.dart';
      16             : 
      17             : 
      18             : const int _kDefaultSize = 1000;
      19             : const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
      20             : 
      21             : /// Class for caching images.
      22             : ///
      23             : /// Implements a least-recently-used cache of up to 1000 images, and up to 100
      24             : /// MB. The maximum size can be adjusted using [maximumSize] and
      25             : /// [maximumSizeBytes].
      26             : ///
      27             : /// The cache also holds a list of 'live' references. An image is considered
      28             : /// live if its [ImageStreamCompleter]'s listener count has never dropped to
      29             : /// zero after adding at least one listener. The cache uses
      30             : /// [ImageStreamCompleter.addOnLastListenerRemovedCallback] to determine when
      31             : /// this has happened.
      32             : ///
      33             : /// The [putIfAbsent] method is the main entry-point to the cache API. It
      34             : /// returns the previously cached [ImageStreamCompleter] for the given key, if
      35             : /// available; if not, it calls the given callback to obtain it first. In either
      36             : /// case, the key is moved to the 'most recently used' position.
      37             : ///
      38             : /// A caller can determine whether an image is already in the cache by using
      39             : /// [containsKey], which will return true if the image is tracked by the cache
      40             : /// in a pending or completed state. More fine grained information is available
      41             : /// by using the [statusForKey] method.
      42             : ///
      43             : /// Generally this class is not used directly. The [ImageProvider] class and its
      44             : /// subclasses automatically handle the caching of images.
      45             : ///
      46             : /// A shared instance of this cache is retained by [PaintingBinding] and can be
      47             : /// obtained via the [imageCache] top-level property in the [painting] library.
      48             : ///
      49             : /// {@tool snippet}
      50             : ///
      51             : /// This sample shows how to supply your own caching logic and replace the
      52             : /// global [imageCache] variable.
      53             : ///
      54             : /// ```dart
      55             : /// /// This is the custom implementation of [ImageCache] where we can override
      56             : /// /// the logic.
      57             : /// class MyImageCache extends ImageCache {
      58             : ///   @override
      59             : ///   void clear() {
      60             : ///     print('Clearing cache!');
      61             : ///     super.clear();
      62             : ///   }
      63             : /// }
      64             : ///
      65             : /// class MyWidgetsBinding extends WidgetsFlutterBinding {
      66             : ///   @override
      67             : ///   ImageCache createImageCache() => MyImageCache();
      68             : /// }
      69             : ///
      70             : /// void main() {
      71             : ///   // The constructor sets global variables.
      72             : ///   MyWidgetsBinding();
      73             : ///   runApp(const MyApp());
      74             : /// }
      75             : ///
      76             : /// class MyApp extends StatelessWidget {
      77             : ///   const MyApp({Key? key}) : super(key: key);
      78             : ///
      79             : ///   @override
      80             : ///   Widget build(BuildContext context) {
      81             : ///     return Container();
      82             : ///   }
      83             : /// }
      84             : /// ```
      85             : /// {@end-tool}
      86             : class ImageCacheExt extends ImageCache {
      87             :   final RemoveAwareMap<Object, _PendingImage> _pendingImages = RemoveAwareMap<Object, _PendingImage>();
      88             :   final RemoveAwareMap<Object, _CachedImage> _cache = RemoveAwareMap<Object, _CachedImage>();
      89             :   /// ImageStreamCompleters with at least one listener. These images may or may
      90             :   /// not fit into the _pendingImages or _cache objects.
      91             :   ///
      92             :   /// Unlike _cache, the [_CachedImage] for this may have a null byte size.
      93             :   final RemoveAwareMap<Object, _LiveImage> _liveImages = RemoveAwareMap<Object, _LiveImage>();
      94             : 
      95           1 :   ImageCacheExt() {
      96           3 :     _pendingImages.hasRemovedCallback = _hasImageRemovedCallback;
      97           3 :     _cache.hasRemovedCallback = _hasImageRemovedCallback;
      98           3 :     _liveImages.hasRemovedCallback = _hasImageRemovedCallback;
      99             :   }
     100             : 
     101             :   Set<Object> _waitingToBeCheckedKeys = Set<Object>();
     102             :   bool _isScheduledImageStatusCheck = false;
     103             : 
     104           1 :   void _hasImageRemovedCallback(dynamic key, dynamic value) {
     105           1 :     if (key is ImageProviderExt) {
     106           2 :       _waitingToBeCheckedKeys.add(key);
     107             :     }
     108           1 :     if (_isScheduledImageStatusCheck) return;
     109           1 :     _isScheduledImageStatusCheck = true;
     110             :     //We should do check in MicroTask to avoid if image is remove and add right away
     111           2 :     scheduleMicrotask(() {
     112           3 :       _waitingToBeCheckedKeys.forEach((key) {
     113           2 :         if (!_pendingImages.containsKey(key) &&
     114           2 :             !_cache.containsKey(key) &&
     115           2 :             !_liveImages.containsKey(key)) {
     116           1 :           if (key is ImageProviderExt) {
     117           1 :             key.dispose();
     118             :           }
     119             :         }
     120             :       });
     121           2 :       _waitingToBeCheckedKeys.clear();
     122           1 :       _isScheduledImageStatusCheck = false;
     123             :     });
     124             :   }
     125             : 
     126             :   /// Maximum number of entries to store in the cache.
     127             :   ///
     128             :   /// Once this many entries have been cached, the least-recently-used entry is
     129             :   /// evicted when adding a new entry.
     130           2 :   int get maximumSize => _maximumSize;
     131             :   int _maximumSize = _kDefaultSize;
     132             :   /// Changes the maximum cache size.
     133             :   ///
     134             :   /// If the new size is smaller than the current number of elements, the
     135             :   /// extraneous elements are evicted immediately. Setting this to zero and then
     136             :   /// returning it to its original value will therefore immediately clear the
     137             :   /// cache.
     138           0 :   set maximumSize(int value) {
     139           0 :     assert(value != null);
     140           0 :     assert(value >= 0);
     141           0 :     if (value == maximumSize)
     142             :       return;
     143             :     TimelineTask? timelineTask;
     144             :     if (!kReleaseMode) {
     145           0 :       timelineTask = TimelineTask()..start(
     146             :         'ImageCache.setMaximumSize',
     147           0 :         arguments: <String, dynamic>{'value': value},
     148             :       );
     149             :     }
     150           0 :     _maximumSize = value;
     151           0 :     if (maximumSize == 0) {
     152           0 :       clear();
     153             :     } else {
     154           0 :       _checkCacheSize(timelineTask);
     155             :     }
     156             :     if (!kReleaseMode) {
     157           0 :       timelineTask!.finish();
     158             :     }
     159             :   }
     160             : 
     161             :   /// The current number of cached entries.
     162           0 :   int get currentSize => _cache.length;
     163             : 
     164             :   /// Maximum size of entries to store in the cache in bytes.
     165             :   ///
     166             :   /// Once more than this amount of bytes have been cached, the
     167             :   /// least-recently-used entry is evicted until there are fewer than the
     168             :   /// maximum bytes.
     169           2 :   int get maximumSizeBytes => _maximumSizeBytes;
     170             :   int _maximumSizeBytes = _kDefaultSizeBytes;
     171             :   /// Changes the maximum cache bytes.
     172             :   ///
     173             :   /// If the new size is smaller than the current size in bytes, the
     174             :   /// extraneous elements are evicted immediately. Setting this to zero and then
     175             :   /// returning it to its original value will therefore immediately clear the
     176             :   /// cache.
     177           0 :   set maximumSizeBytes(int value) {
     178           0 :     assert(value != null);
     179           0 :     assert(value >= 0);
     180           0 :     if (value == _maximumSizeBytes)
     181             :       return;
     182             :     TimelineTask? timelineTask;
     183             :     if (!kReleaseMode) {
     184           0 :       timelineTask = TimelineTask()..start(
     185             :         'ImageCache.setMaximumSizeBytes',
     186           0 :         arguments: <String, dynamic>{'value': value},
     187             :       );
     188             :     }
     189           0 :     _maximumSizeBytes = value;
     190           0 :     if (_maximumSizeBytes == 0) {
     191           0 :       clear();
     192             :     } else {
     193           0 :       _checkCacheSize(timelineTask);
     194             :     }
     195             :     if (!kReleaseMode) {
     196           0 :       timelineTask!.finish();
     197             :     }
     198             :   }
     199             : 
     200             :   /// The current size of cached entries in bytes.
     201           0 :   int get currentSizeBytes => _currentSizeBytes;
     202             :   int _currentSizeBytes = 0;
     203             : 
     204             :   /// Evicts all pending and keepAlive entries from the cache.
     205             :   ///
     206             :   /// This is useful if, for instance, the root asset bundle has been updated
     207             :   /// and therefore new images must be obtained.
     208             :   ///
     209             :   /// Images which have not finished loading yet will not be removed from the
     210             :   /// cache, and when they complete they will be inserted as normal.
     211             :   ///
     212             :   /// This method does not clear live references to images, since clearing those
     213             :   /// would not reduce memory pressure. Such images still have listeners in the
     214             :   /// application code, and will still remain resident in memory.
     215             :   ///
     216             :   /// To clear live references, use [clearLiveImages].
     217           0 :   void clear() {
     218             :     if (!kReleaseMode) {
     219           0 :       Timeline.instantSync(
     220             :         'ImageCache.clear',
     221           0 :         arguments: <String, dynamic>{
     222           0 :           'pendingImages': _pendingImages.length,
     223           0 :           'keepAliveImages': _cache.length,
     224           0 :           'liveImages': _liveImages.length,
     225           0 :           'currentSizeInBytes': _currentSizeBytes,
     226             :         },
     227             :       );
     228             :     }
     229           0 :     for (final _CachedImage image in _cache.values) {
     230           0 :       image.dispose();
     231             :     }
     232           0 :     _cache.clear();
     233           0 :     _pendingImages.clear();
     234           0 :     _currentSizeBytes = 0;
     235             :   }
     236             : 
     237             :   /// Evicts a single entry from the cache, returning true if successful.
     238             :   ///
     239             :   /// Pending images waiting for completion are removed as well, returning true
     240             :   /// if successful. When a pending image is removed the listener on it is
     241             :   /// removed as well to prevent it from adding itself to the cache if it
     242             :   /// eventually completes.
     243             :   ///
     244             :   /// If this method removes a pending image, it will also remove
     245             :   /// the corresponding live tracking of the image, since it is no longer clear
     246             :   /// if the image will ever complete or have any listeners, and failing to
     247             :   /// remove the live reference could leave the cache in a state where all
     248             :   /// subsequent calls to [putIfAbsent] will return an [ImageStreamCompleter]
     249             :   /// that will never complete.
     250             :   ///
     251             :   /// If this method removes a completed image, it will _not_ remove the live
     252             :   /// reference to the image, which will only be cleared when the listener
     253             :   /// count on the completer drops to zero. To clear live image references,
     254             :   /// whether completed or not, use [clearLiveImages].
     255             :   ///
     256             :   /// The `key` must be equal to an object used to cache an image in
     257             :   /// [ImageCache.putIfAbsent].
     258             :   ///
     259             :   /// If the key is not immediately available, as is common, consider using
     260             :   /// [ImageProvider.evict] to call this method indirectly instead.
     261             :   ///
     262             :   /// The `includeLive` argument determines whether images that still have
     263             :   /// listeners in the tree should be evicted as well. This parameter should be
     264             :   /// set to true in cases where the image may be corrupted and needs to be
     265             :   /// completely discarded by the cache. It should be set to false when calls
     266             :   /// to evict are trying to relieve memory pressure, since an image with a
     267             :   /// listener will not actually be evicted from memory, and subsequent attempts
     268             :   /// to load it will end up allocating more memory for the image again. The
     269             :   /// argument must not be null.
     270             :   ///
     271             :   /// See also:
     272             :   ///
     273             :   ///  * [ImageProvider], for providing images to the [Image] widget.
     274           1 :   bool evict(Object key, { bool includeLive = true }) {
     275           0 :     assert(includeLive != null);
     276             :     if (includeLive) {
     277             :       // Remove from live images - the cache will not be able to mark
     278             :       // it as complete, and it might be getting evicted because it
     279             :       // will never complete, e.g. it was loaded in a FakeAsync zone.
     280             :       // In such a case, we need to make sure subsequent calls to
     281             :       // putIfAbsent don't return this image that may never complete.
     282           2 :       final _LiveImage? image = _liveImages.remove(key);
     283           1 :       image?.dispose();
     284             :     }
     285           2 :     final _PendingImage? pendingImage = _pendingImages.remove(key);
     286             :     if (pendingImage != null) {
     287             :       if (!kReleaseMode) {
     288           2 :         Timeline.instantSync('ImageCache.evict', arguments: <String, dynamic>{
     289             :           'type': 'pending',
     290             :         });
     291             :       }
     292           1 :       pendingImage.removeListener();
     293             :       return true;
     294             :     }
     295           0 :     final _CachedImage? image = _cache.remove(key);
     296             :     if (image != null) {
     297             :       if (!kReleaseMode) {
     298           0 :         Timeline.instantSync('ImageCache.evict', arguments: <String, dynamic>{
     299             :           'type': 'keepAlive',
     300           0 :           'sizeInBytes': image.sizeBytes,
     301             :         });
     302             :       }
     303           0 :       _currentSizeBytes -= image.sizeBytes!;
     304           0 :       image.dispose();
     305             :       return true;
     306             :     }
     307             :     if (!kReleaseMode) {
     308           0 :       Timeline.instantSync('ImageCache.evict', arguments: <String, dynamic>{
     309             :         'type': 'miss',
     310             :       });
     311             :     }
     312             :     return false;
     313             :   }
     314             : 
     315             :   /// Updates the least recently used image cache with this image, if it is
     316             :   /// less than the [maximumSizeBytes] of this cache.
     317             :   ///
     318             :   /// Resizes the cache as appropriate to maintain the constraints of
     319             :   /// [maximumSize] and [maximumSizeBytes].
     320           0 :   void _touch(Object key, _CachedImage image, TimelineTask? timelineTask) {
     321           0 :     assert(timelineTask != null);
     322           0 :     if (image.sizeBytes != null && image.sizeBytes! <= maximumSizeBytes && maximumSize > 0) {
     323           0 :       _currentSizeBytes += image.sizeBytes!;
     324           0 :       _cache[key] = image;
     325           0 :       _checkCacheSize(timelineTask);
     326             :     } else {
     327           0 :       image.dispose();
     328             :     }
     329             :   }
     330             : 
     331           1 :   void _trackLiveImage(Object key, ImageStreamCompleter completer, int? sizeBytes) {
     332             :     // Avoid adding unnecessary callbacks to the completer.
     333           3 :     _liveImages.putIfAbsent(key, () {
     334             :       // Even if no callers to ImageProvider.resolve have listened to the stream,
     335             :       // the cache is listening to the stream and will remove itself once the
     336             :       // image completes to move it from pending to keepAlive.
     337             :       // Even if the cache size is 0, we still add this tracker, which will add
     338             :       // a keep alive handle to the stream.
     339           1 :       return _LiveImage(
     340             :         completer,
     341           0 :             () {
     342           0 :           _liveImages.remove(key);
     343             :         },
     344             :       );
     345           1 :     }).sizeBytes ??= sizeBytes;
     346             :   }
     347             : 
     348             :   /// Returns the previously cached [ImageStream] for the given key, if available;
     349             :   /// if not, calls the given callback to obtain it first. In either case, the
     350             :   /// key is moved to the 'most recently used' position.
     351             :   ///
     352             :   /// The arguments must not be null. The `loader` cannot return null.
     353             :   ///
     354             :   /// In the event that the loader throws an exception, it will be caught only if
     355             :   /// `onError` is also provided. When an exception is caught resolving an image,
     356             :   /// no completers are cached and `null` is returned instead of a new
     357             :   /// completer.
     358           1 :   ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {
     359           0 :     assert(key != null);
     360           0 :     assert(loader != null);
     361             :     TimelineTask? timelineTask;
     362             :     TimelineTask? listenerTask;
     363             :     if (!kReleaseMode) {
     364           2 :       timelineTask = TimelineTask()..start(
     365             :         'ImageCache.putIfAbsent',
     366           1 :         arguments: <String, dynamic>{
     367           1 :           'key': key.toString(),
     368             :         },
     369             :       );
     370             :     }
     371           2 :     ImageStreamCompleter? result = _pendingImages[key]?.completer;
     372             :     // Nothing needs to be done because the image hasn't loaded yet.
     373             :     if (result != null) {
     374             :       if (!kReleaseMode) {
     375           0 :         timelineTask!.finish(arguments: <String, dynamic>{'result': 'pending'});
     376             :       }
     377             :       return result;
     378             :     }
     379             :     // Remove the provider from the list so that we can move it to the
     380             :     // recently used position below.
     381             :     // Don't use _touch here, which would trigger a check on cache size that is
     382             :     // not needed since this is just moving an existing cache entry to the head.
     383           2 :     final _CachedImage? image = _cache.remove(key);
     384             :     if (image != null) {
     385             :       if (!kReleaseMode) {
     386           0 :         timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
     387             :       }
     388             :       // The image might have been keptAlive but had no listeners (so not live).
     389             :       // Make sure the cache starts tracking it as live again.
     390           0 :       _trackLiveImage(
     391             :         key,
     392           0 :         image.completer,
     393           0 :         image.sizeBytes,
     394             :       );
     395           0 :       _cache[key] = image;
     396           0 :       return image.completer;
     397             :     }
     398             : 
     399           2 :     final _LiveImage? liveImage = _liveImages[key];
     400             :     if (liveImage != null) {
     401           0 :       _touch(
     402             :         key,
     403           0 :         _CachedImage(
     404           0 :           liveImage.completer,
     405           0 :           sizeBytes: liveImage.sizeBytes,
     406             :         ),
     407             :         timelineTask,
     408             :       );
     409             :       if (!kReleaseMode) {
     410           0 :         timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
     411             :       }
     412           0 :       return liveImage.completer;
     413             :     }
     414             : 
     415             :     try {
     416             :       result = loader();
     417           1 :       _trackLiveImage(key, result, null);
     418             :     } catch (error, stackTrace) {
     419             :       if (!kReleaseMode) {
     420           0 :         timelineTask!.finish(arguments: <String, dynamic>{
     421             :           'result': 'error',
     422           0 :           'error': error.toString(),
     423           0 :           'stackTrace': stackTrace.toString(),
     424             :         });
     425             :       }
     426             :       if (onError != null) {
     427             :         onError(error, stackTrace);
     428             :         return null;
     429             :       } else {
     430             :         rethrow;
     431             :       }
     432             :     }
     433             : 
     434             :     if (!kReleaseMode) {
     435           2 :       listenerTask = TimelineTask(parent: timelineTask)..start('listener');
     436             :     }
     437             :     // If we're doing tracing, we need to make sure that we don't try to finish
     438             :     // the trace entry multiple times if we get re-entrant calls from a multi-
     439             :     // frame provider here.
     440             :     bool listenedOnce = false;
     441             : 
     442             :     // We shouldn't use the _pendingImages map if the cache is disabled, but we
     443             :     // will have to listen to the image at least once so we don't leak it in
     444             :     // the live image tracking.
     445             :     // If the cache is disabled, this variable will be set.
     446             :     _PendingImage? untrackedPendingImage;
     447           0 :     void listener(ImageInfo? info, bool syncCall) {
     448             :       int? sizeBytes;
     449             :       if (info != null) {
     450           0 :         if (info is PowerImageInfo) {
     451           0 :           sizeBytes = info.width! * info.height! * 4;
     452             :         } else {
     453           0 :           sizeBytes = info.image.height * info.image.width * 4;
     454             :         }
     455           0 :         info.dispose();
     456             :       }
     457           0 :       final _CachedImage image = _CachedImage(
     458             :         result!,
     459             :         sizeBytes: sizeBytes,
     460             :       );
     461             : 
     462           0 :       _trackLiveImage(key, result, sizeBytes);
     463             : 
     464             :       // Only touch if the cache was enabled when resolve was initially called.
     465             :       if (untrackedPendingImage == null) {
     466           0 :         _touch(key, image, listenerTask);
     467             :       } else {
     468           0 :         image.dispose();
     469             :       }
     470             : 
     471           0 :       final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
     472             :       if (pendingImage != null) {
     473           0 :         pendingImage.removeListener();
     474             :       }
     475             :       if (!kReleaseMode && !listenedOnce) {
     476           0 :         listenerTask!.finish(arguments: <String, dynamic>{
     477             :           'syncCall': syncCall,
     478             :           'sizeInBytes': sizeBytes,
     479             :         });
     480           0 :         timelineTask!.finish(arguments: <String, dynamic>{
     481           0 :           'currentSizeBytes': currentSizeBytes,
     482           0 :           'currentSize': currentSize,
     483             :         });
     484             :       }
     485             :       listenedOnce = true;
     486             :     }
     487             : 
     488           1 :     final ImageStreamListener streamListener = ImageStreamListener(listener);
     489           4 :     if (maximumSize > 0 && maximumSizeBytes > 0) {
     490           3 :       _pendingImages[key] = _PendingImage(result, streamListener);
     491             :     } else {
     492           0 :       untrackedPendingImage = _PendingImage(result, streamListener);
     493             :     }
     494             :     // Listener is removed in [_PendingImage.removeListener].
     495           1 :     result.addListener(streamListener);
     496             : 
     497             :     return result;
     498             :   }
     499             : 
     500             :   /// The [ImageCacheStatus] information for the given `key`.
     501             :   // ImageCacheStatus statusForKey(Object key) {
     502             :   //   return ImageCacheStatus._(
     503             :   //     pending: _pendingImages.containsKey(key),
     504             :   //     keepAlive: _cache.containsKey(key),
     505             :   //     live: _liveImages.containsKey(key),
     506             :   //   );
     507             :   // }
     508             : 
     509             :   /// Returns whether this `key` has been previously added by [putIfAbsent].
     510           0 :   bool containsKey(Object key) {
     511           0 :     return _pendingImages[key] != null || _cache[key] != null;
     512             :   }
     513             : 
     514             :   /// The number of live images being held by the [ImageCache].
     515             :   ///
     516             :   /// Compare with [ImageCache.currentSize] for keepAlive images.
     517           0 :   int get liveImageCount => _liveImages.length;
     518             : 
     519             :   /// The number of images being tracked as pending in the [ImageCache].
     520             :   ///
     521             :   /// Compare with [ImageCache.currentSize] for keepAlive images.
     522           0 :   int get pendingImageCount => _pendingImages.length;
     523             : 
     524             :   /// Clears any live references to images in this cache.
     525             :   ///
     526             :   /// An image is considered live if its [ImageStreamCompleter] has never hit
     527             :   /// zero listeners after adding at least one listener. The
     528             :   /// [ImageStreamCompleter.addOnLastListenerRemovedCallback] is used to
     529             :   /// determine when this has happened.
     530             :   ///
     531             :   /// This is called after a hot reload to evict any stale references to image
     532             :   /// data for assets that have changed. Calling this method does not relieve
     533             :   /// memory pressure, since the live image caching only tracks image instances
     534             :   /// that are also being held by at least one other object.
     535           0 :   void clearLiveImages() {
     536           0 :     for (final _LiveImage image in _liveImages.values) {
     537           0 :       image.dispose();
     538             :     }
     539           0 :     _liveImages.clear();
     540             :   }
     541             : 
     542             :   // Remove images from the cache until both the length and bytes are below
     543             :   // maximum, or the cache is empty.
     544           0 :   void _checkCacheSize(TimelineTask? timelineTask) {
     545           0 :     final Map<String, dynamic> finishArgs = <String, dynamic>{};
     546             :     TimelineTask? checkCacheTask;
     547             :     if (!kReleaseMode) {
     548           0 :       checkCacheTask = TimelineTask(parent: timelineTask)..start('checkCacheSize');
     549           0 :       finishArgs['evictedKeys'] = <String>[];
     550           0 :       finishArgs['currentSize'] = currentSize;
     551           0 :       finishArgs['currentSizeBytes'] = currentSizeBytes;
     552             :     }
     553           0 :     while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
     554           0 :       final Object key = _cache.keys.first;
     555           0 :       final _CachedImage image = _cache[key]!;
     556           0 :       _currentSizeBytes -= image.sizeBytes!;
     557           0 :       image.dispose();
     558           0 :       _cache.remove(key);
     559             :       if (!kReleaseMode) {
     560           0 :         (finishArgs['evictedKeys'] as List<String>).add(key.toString());
     561             :       }
     562             :     }
     563             :     if (!kReleaseMode) {
     564           0 :       finishArgs['endSize'] = currentSize;
     565           0 :       finishArgs['endSizeBytes'] = currentSizeBytes;
     566           0 :       checkCacheTask!.finish(arguments: finishArgs);
     567             :     }
     568           0 :     assert(_currentSizeBytes >= 0);
     569           0 :     assert(_cache.length <= maximumSize);
     570           0 :     assert(_currentSizeBytes <= maximumSizeBytes);
     571             :   }
     572             : }
     573             : 
     574             : /// Information about how the [ImageCache] is tracking an image.
     575             : ///
     576             : /// A [pending] image is one that has not completed yet. It may also be tracked
     577             : /// as [live] because something is listening to it.
     578             : ///
     579             : /// A [keepAlive] image is being held in the cache, which uses Least Recently
     580             : /// Used semantics to determine when to evict an image. These images are subject
     581             : /// to eviction based on [ImageCache.maximumSizeBytes] and
     582             : /// [ImageCache.maximumSize]. It may be [live], but not [pending].
     583             : ///
     584             : /// A [live] image is being held until its [ImageStreamCompleter] has no more
     585             : /// listeners. It may also be [pending] or [keepAlive].
     586             : ///
     587             : /// An [untracked] image is not being cached.
     588             : ///
     589             : /// To obtain an [ImageCacheStatus], use [ImageCache.statusForKey] or
     590             : /// [ImageProvider.obtainCacheStatus].
     591             : @immutable
     592             : class ImageCacheStatus {
     593           0 :   const ImageCacheStatus._({
     594             :     this.pending = false,
     595             :     this.keepAlive = false,
     596             :     this.live = false,
     597           0 :   }) : assert(!pending || !keepAlive);
     598             : 
     599             :   /// An image that has been submitted to [ImageCache.putIfAbsent], but
     600             :   /// not yet completed.
     601             :   final bool pending;
     602             : 
     603             :   /// An image that has been submitted to [ImageCache.putIfAbsent], has
     604             :   /// completed, fits based on the sizing rules of the cache, and has not been
     605             :   /// evicted.
     606             :   ///
     607             :   /// Such images will be kept alive even if [live] is false, as long
     608             :   /// as they have not been evicted from the cache based on its sizing rules.
     609             :   final bool keepAlive;
     610             : 
     611             :   /// An image that has been submitted to [ImageCache.putIfAbsent] and has at
     612             :   /// least one listener on its [ImageStreamCompleter].
     613             :   ///
     614             :   /// Such images may also be [keepAlive] if they fit in the cache based on its
     615             :   /// sizing rules. They may also be [pending] if they have not yet resolved.
     616             :   final bool live;
     617             : 
     618             :   /// An image that is tracked in some way by the [ImageCache], whether
     619             :   /// [pending], [keepAlive], or [live].
     620           0 :   bool get tracked => pending || keepAlive || live;
     621             : 
     622             :   /// An image that either has not been submitted to
     623             :   /// [ImageCache.putIfAbsent] or has otherwise been evicted from the
     624             :   /// [keepAlive] and [live] caches.
     625           0 :   bool get untracked => !pending && !keepAlive && !live;
     626             : 
     627           0 :   @override
     628             :   bool operator ==(Object other) {
     629           0 :     if (other.runtimeType != runtimeType) {
     630             :       return false;
     631             :     }
     632           0 :     return other is ImageCacheStatus
     633           0 :         && other.pending == pending
     634           0 :         && other.keepAlive == keepAlive
     635           0 :         && other.live == live;
     636             :   }
     637             : 
     638           0 :   @override
     639           0 :   int get hashCode => hashValues(pending, keepAlive, live);
     640             : 
     641           0 :   @override
     642           0 :   String toString() => '${objectRuntimeType(this, 'ImageCacheStatus')}(pending: $pending, live: $live, keepAlive: $keepAlive)';
     643             : }
     644             : 
     645             : /// Base class for [_CachedImage] and [_LiveImage].
     646             : ///
     647             : /// Exists primarily so that a [_LiveImage] cannot be added to the
     648             : /// [ImageCache._cache].
     649             : abstract class _CachedImageBase {
     650           1 :   _CachedImageBase(
     651             :       this.completer, {
     652             :         this.sizeBytes,
     653           0 :       }) : assert(completer != null),
     654           1 :         handle = completer.keepAlive();
     655             : 
     656             :   final ImageStreamCompleter completer;
     657             :   int? sizeBytes;
     658             :   ImageStreamCompleterHandle? handle;
     659             : 
     660           1 :   @mustCallSuper
     661             :   void dispose() {
     662           1 :     assert(handle != null);
     663             :     // Give any interested parties a chance to listen to the stream before we
     664             :     // potentially dispose it.
     665           2 :     SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
     666           0 :       assert(handle != null);
     667           0 :       handle?.dispose();
     668           0 :       handle = null;
     669             :     });
     670             :   }
     671             : }
     672             : 
     673             : class _CachedImage extends _CachedImageBase {
     674           0 :   _CachedImage(ImageStreamCompleter completer, {int? sizeBytes})
     675           0 :       : super(completer, sizeBytes: sizeBytes);
     676             : }
     677             : 
     678             : class _LiveImage extends _CachedImageBase {
     679           1 :   _LiveImage(ImageStreamCompleter completer, VoidCallback handleRemove, {int? sizeBytes})
     680           1 :       : super(completer, sizeBytes: sizeBytes) {
     681           1 :     _handleRemove = () {
     682             :       handleRemove();
     683           0 :       dispose();
     684             :     };
     685           2 :     completer.addOnLastListenerRemovedCallback(_handleRemove);
     686             :   }
     687             : 
     688             :   late VoidCallback _handleRemove;
     689             : 
     690           1 :   @override
     691             :   void dispose() {
     692           3 :     completer.removeOnLastListenerRemovedCallback(_handleRemove);
     693           1 :     super.dispose();
     694             :   }
     695             : 
     696           0 :   @override
     697           0 :   String toString() => describeIdentity(this);
     698             : }
     699             : 
     700             : class _PendingImage {
     701           1 :   _PendingImage(this.completer, this.listener);
     702             : 
     703             :   final ImageStreamCompleter completer;
     704             :   final ImageStreamListener listener;
     705             : 
     706           1 :   void removeListener() {
     707           3 :     completer.removeListener(listener);
     708             :   }
     709             : }

Generated by: LCOV version 1.15