33// found in the LICENSE file.
44
55import 'dart:developer' ;
6+ import 'dart:ui' show hashValues;
67
78import 'package:flutter/foundation.dart' ;
89
@@ -15,18 +16,24 @@ const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
1516///
1617/// Implements a least-recently-used cache of up to 1000 images, and up to 100
1718/// MB. The maximum size can be adjusted using [maximumSize] and
18- /// [maximumSizeBytes] . Images that are actively in use (i.e. to which the
19- /// application is holding references, either via [ImageStream] objects,
20- /// [ImageStreamCompleter] objects, [ImageInfo] objects, or raw [dart:ui.Image]
21- /// objects) may get evicted from the cache (and thus need to be refetched from
22- /// the network if they are referenced in the [putIfAbsent] method), but the raw
23- /// bits are kept in memory for as long as the application is using them.
19+ /// [maximumSizeBytes] .
20+ ///
21+ /// The cache also holds a list of "live" references. An image is considered
22+ /// live if its [ImageStreamCompleter] 's listener count has never dropped to
23+ /// zero after adding at least one listener. The cache uses
24+ /// [ImageStreamCompleter.addOnLastListenerRemovedCallback] to determine when
25+ /// this has happened.
2426///
2527/// The [putIfAbsent] method is the main entry-point to the cache API. It
2628/// returns the previously cached [ImageStreamCompleter] for the given key, if
2729/// available; if not, it calls the given callback to obtain it first. In either
2830/// case, the key is moved to the "most recently used" position.
2931///
32+ /// A caller can determine whether an image is already in the cache by using
33+ /// [containsKey] , which will return true if the image is tracked by the cache
34+ /// in a pending or compelted state. More fine grained information is available
35+ /// by using the [statusForKey] method.
36+ ///
3037/// Generally this class is not used directly. The [ImageProvider] class and its
3138/// subclasses automatically handle the caching of images.
3239///
@@ -71,6 +78,11 @@ const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
7178class ImageCache {
7279 final Map <Object , _PendingImage > _pendingImages = < Object , _PendingImage > {};
7380 final Map <Object , _CachedImage > _cache = < Object , _CachedImage > {};
81+ /// ImageStreamCompleters with at least one listener. These images may or may
82+ /// not fit into the _pendingImages or _cache objects.
83+ ///
84+ /// Unlike _cache, the [_CachedImage] for this may have a null byte size.
85+ final Map <Object , _CachedImage > _liveImages = < Object , _CachedImage > {};
7486
7587 /// Maximum number of entries to store in the cache.
7688 ///
@@ -163,7 +175,8 @@ class ImageCache {
163175 'ImageCache.clear' ,
164176 arguments: < String , dynamic > {
165177 'pendingImages' : _pendingImages.length,
166- 'cachedImages' : _cache.length,
178+ 'keepAliveImages' : _cache.length,
179+ 'liveImages' : _liveImages.length,
167180 'currentSizeInBytes' : _currentSizeBytes,
168181 },
169182 );
@@ -204,7 +217,7 @@ class ImageCache {
204217 if (image != null ) {
205218 if (! kReleaseMode) {
206219 Timeline .instantSync ('ImageCache.evict' , arguments: < String , dynamic > {
207- 'type' : 'completed ' ,
220+ 'type' : 'keepAlive ' ,
208221 'sizeiInBytes' : image.sizeBytes,
209222 });
210223 }
@@ -219,6 +232,36 @@ class ImageCache {
219232 return false ;
220233 }
221234
235+ /// Updates the least recently used image cache with this image, if it is
236+ /// less than the [maximumSizeBytes] of this cache.
237+ ///
238+ /// Resizes the cache as appropriate to maintain the constraints of
239+ /// [maximumSize] and [maximumSizeBytes] .
240+ void _touch (Object key, _CachedImage image, TimelineTask timelineTask) {
241+ assert (timelineTask != null );
242+ if (image.sizeBytes != null && image.sizeBytes <= maximumSizeBytes) {
243+ _currentSizeBytes += image.sizeBytes;
244+ _cache[key] = image;
245+ _checkCacheSize (timelineTask);
246+ }
247+ }
248+
249+ void _trackLiveImage (Object key, _CachedImage image) {
250+ // Avoid adding unnecessary callbacks to the completer.
251+ if (_liveImages.containsKey (key)) {
252+ assert (identical (_liveImages[key].completer, image.completer));
253+ return ;
254+ }
255+ _liveImages[key] = image;
256+ // Even if no callers to ImageProvider.resolve have listened to the stream,
257+ // the cache is listening to the stream and will remove itself once the
258+ // image completes to move it from pending to keepAlive.
259+ // Even if the cache size is 0, we still add this listener.
260+ image.completer.addOnLastListenerRemovedCallback (() {
261+ _liveImages.remove (key);
262+ });
263+ }
264+
222265 /// Returns the previously cached [ImageStream] for the given key, if available;
223266 /// if not, calls the given callback to obtain it first. In either case, the
224267 /// key is moved to the "most recently used" position.
@@ -255,14 +298,27 @@ class ImageCache {
255298 final _CachedImage image = _cache.remove (key);
256299 if (image != null ) {
257300 if (! kReleaseMode) {
258- timelineTask.finish (arguments: < String , dynamic > {'result' : 'completed ' });
301+ timelineTask.finish (arguments: < String , dynamic > {'result' : 'keepAlive ' });
259302 }
303+ // The image might have been keptAlive but had no listeners. We should
304+ // track it as live again until it has no listeners again.
305+ _trackLiveImage (key, image);
260306 _cache[key] = image;
261307 return image.completer;
262308 }
263309
310+ final _CachedImage liveImage = _liveImages[key];
311+ if (liveImage != null ) {
312+ _touch (key, liveImage, timelineTask);
313+ if (! kReleaseMode) {
314+ timelineTask.finish (arguments: < String , dynamic > {'result' : 'keepAlive' });
315+ }
316+ return liveImage.completer;
317+ }
318+
264319 try {
265320 result = loader ();
321+ _trackLiveImage (key, _CachedImage (result, null ));
266322 } catch (error, stackTrace) {
267323 if (! kReleaseMode) {
268324 timelineTask.finish (arguments: < String , dynamic > {
@@ -282,21 +338,37 @@ class ImageCache {
282338 if (! kReleaseMode) {
283339 listenerTask = TimelineTask (parent: timelineTask)..start ('listener' );
284340 }
341+ // If we're doing tracing, we need to make sure that we don't try to finish
342+ // the trace entry multiple times if we get re-entrant calls from a multi-
343+ // frame provider here.
285344 bool listenedOnce = false ;
345+
346+ // We shouldn't use the _pendingImages map if the cache is disabled, but we
347+ // will have to listen to the image at least once so we don't leak it in
348+ // the live image tracking.
349+ // If the cache is disabled, this variable will be set.
350+ _PendingImage untrackedPendingImage;
286351 void listener (ImageInfo info, bool syncCall) {
287352 // Images that fail to load don't contribute to cache size.
288353 final int imageSize = info? .image == null ? 0 : info.image.height * info.image.width * 4 ;
354+
289355 final _CachedImage image = _CachedImage (result, imageSize);
290- final _PendingImage pendingImage = _pendingImages.remove (key);
356+ if (! _liveImages.containsKey (key)) {
357+ assert (syncCall);
358+ result.addOnLastListenerRemovedCallback (() {
359+ _liveImages.remove (key);
360+ });
361+ }
362+ _liveImages[key] = image;
363+ final _PendingImage pendingImage = untrackedPendingImage ?? _pendingImages.remove (key);
291364 if (pendingImage != null ) {
292365 pendingImage.removeListener ();
293366 }
294-
295- if (imageSize <= maximumSizeBytes) {
296- _currentSizeBytes += imageSize;
297- _cache[key] = image;
298- _checkCacheSize (listenerTask);
367+ // Only touch if the cache was enabled when resolve was initially called.
368+ if (untrackedPendingImage == null ) {
369+ _touch (key, image, listenerTask);
299370 }
371+
300372 if (! kReleaseMode && ! listenedOnce) {
301373 listenerTask.finish (arguments: < String , dynamic > {
302374 'syncCall' : syncCall,
@@ -309,20 +381,58 @@ class ImageCache {
309381 }
310382 listenedOnce = true ;
311383 }
384+
385+ final ImageStreamListener streamListener = ImageStreamListener (listener);
312386 if (maximumSize > 0 && maximumSizeBytes > 0 ) {
313- final ImageStreamListener streamListener = ImageStreamListener (listener);
314387 _pendingImages[key] = _PendingImage (result, streamListener);
315- // Listener is removed in [_PendingImage.removeListener].
316- result. addListener ( streamListener);
388+ } else {
389+ untrackedPendingImage = _PendingImage (result, streamListener);
317390 }
391+ // Listener is removed in [_PendingImage.removeListener].
392+ result.addListener (streamListener);
393+
318394 return result;
319395 }
320396
397+ /// The [ImageCacheStatus] information for the given `key` .
398+ ImageCacheStatus statusForKey (Object key) {
399+ return ImageCacheStatus ._(
400+ pending: _pendingImages.containsKey (key),
401+ keepAlive: _cache.containsKey (key),
402+ live: _liveImages.containsKey (key),
403+ );
404+ }
405+
321406 /// Returns whether this `key` has been previously added by [putIfAbsent] .
322407 bool containsKey (Object key) {
323408 return _pendingImages[key] != null || _cache[key] != null ;
324409 }
325410
411+ /// The number of live images being held by the [ImageCache] .
412+ ///
413+ /// Compare with [ImageCache.currentSize] for keepAlive images.
414+ int get liveImageCount => _liveImages.length;
415+
416+ /// The number of images being tracked as pending in the [ImageCache] .
417+ ///
418+ /// Compare with [ImageCache.currentSize] for keepAlive images.
419+ int get pendingImageCount => _pendingImages.length;
420+
421+ /// Clears any live references to images in this cache.
422+ ///
423+ /// An image is considered live if its [ImageStreamCompleter] has never hit
424+ /// zero listeners after adding at least one listener. The
425+ /// [ImageStreamCompleter.addOnLastListenerRemovedCallback] is used to
426+ /// determine when this has happened.
427+ ///
428+ /// This is called after a hot reload to evict any stale references to image
429+ /// data for assets that have changed. Calling this method does not relieve
430+ /// memory pressure, since the live image caching only tracks image instances
431+ /// that are also being held by at least one other object.
432+ void clearLiveImages () {
433+ _liveImages.clear ();
434+ }
435+
326436 // Remove images from the cache until both the length and bytes are below
327437 // maximum, or the cache is empty.
328438 void _checkCacheSize (TimelineTask timelineTask) {
@@ -354,6 +464,73 @@ class ImageCache {
354464 }
355465}
356466
467+ /// Information about how the [ImageCache] is tracking an image.
468+ ///
469+ /// A [pending] image is one that has not completed yet. It may also be tracked
470+ /// as [live] because something is listening to it.
471+ ///
472+ /// A [keepAlive] image is being held in the cache, which uses Least Recently
473+ /// Used semantics to determine when to evict an image. These images are subject
474+ /// to eviction based on [ImageCache.maximumSizeBytes] and
475+ /// [ImageCache.maximumSize] . It may be [live] , but not [pending] .
476+ ///
477+ /// A [live] image is being held until its [ImageStreamCompleter] has no more
478+ /// listeners. It may also be [pending] or [keepAlive] .
479+ ///
480+ /// An [untracked] image is not being cached.
481+ ///
482+ /// To obtain an [ImageCacheStatus] , use [ImageCache.statusForKey] or
483+ /// [ImageProvider.obtainCacheStatus] .
484+ class ImageCacheStatus {
485+ const ImageCacheStatus ._({
486+ this .pending = false ,
487+ this .keepAlive = false ,
488+ this .live = false ,
489+ }) : assert (! pending || ! keepAlive);
490+
491+ /// An image that has been submitted to [ImageCache.putIfAbsent] , but
492+ /// not yet completed.
493+ final bool pending;
494+
495+ /// An image that has been submitted to [ImageCache.putIfAbsent] , has
496+ /// completed, fits based on the sizing rules of the cache, and has not been
497+ /// evicted.
498+ ///
499+ /// Such images will be kept alive even if [live] is false, as long
500+ /// as they have not been evicted from the cache based on its sizing rules.
501+ final bool keepAlive;
502+
503+ /// An image that has been submitted to [ImageCache.putIfAbsent] and has at
504+ /// least one listener on its [ImageStreamCompleter] .
505+ ///
506+ /// Such images may also be [keepAlive] if they fit in the cache based on its
507+ /// sizing rules. They may also be [pending] if they have not yet resolved.
508+ final bool live;
509+
510+ /// An image that is tracked in some way by the [ImageCache] , whether
511+ /// [pending] , [keepAlive] , or [live] .
512+ bool get tracked => pending || keepAlive || live;
513+
514+ /// An image that either has not been submitted to
515+ /// [ImageCache.putIfAbsent] or has otherwise been evicted from the
516+ /// [keepAlive] and [live] caches.
517+ bool get untracked => ! pending && ! keepAlive && ! live;
518+
519+ @override
520+ bool operator == (Object other) {
521+ if (other.runtimeType != runtimeType) {
522+ return false ;
523+ }
524+ return other is ImageCacheStatus
525+ && other.pending == pending
526+ && other.keepAlive == keepAlive
527+ && other.live == live;
528+ }
529+
530+ @override
531+ int get hashCode => hashValues (pending, keepAlive, live);
532+ }
533+
357534class _CachedImage {
358535 _CachedImage (this .completer, this .sizeBytes);
359536
0 commit comments