diff --git a/packages/flutter/lib/src/painting/image_cache.dart b/packages/flutter/lib/src/painting/image_cache.dart index f2068ae0f2c7..f2327148dc85 100644 --- a/packages/flutter/lib/src/painting/image_cache.dart +++ b/packages/flutter/lib/src/painting/image_cache.dart @@ -397,16 +397,16 @@ class ImageCache { if (!kReleaseMode) { listenerTask = TimelineTask(parent: timelineTask)..start('listener'); } - // If we're doing tracing, we need to make sure that we don't try to finish - // the trace entry multiple times if we get re-entrant calls from a multi- - // frame provider here. + // A multi-frame provider may call the listener more than once. We need do make + // sure that some cleanup works won't run multiple times, such as finishing the + // tracing task or removing the listeners bool listenedOnce = false; // We shouldn't use the _pendingImages map if the cache is disabled, but we // will have to listen to the image at least once so we don't leak it in // the live image tracking. - // If the cache is disabled, this variable will be set. - _PendingImage? untrackedPendingImage; + final bool trackPendingImage = maximumSize > 0 && maximumSizeBytes > 0; + late _PendingImage pendingImage; void listener(ImageInfo? info, bool syncCall) { int? sizeBytes; if (info != null) { @@ -421,14 +421,14 @@ class ImageCache { _trackLiveImage(key, result, sizeBytes); // Only touch if the cache was enabled when resolve was initially called. - if (untrackedPendingImage == null) { + if (trackPendingImage) { _touch(key, image, listenerTask); } else { image.dispose(); } - final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key); - if (pendingImage != null) { + _pendingImages.remove(key); + if (!listenedOnce) { pendingImage.removeListener(); } if (!kReleaseMode && !listenedOnce) { @@ -445,10 +445,9 @@ class ImageCache { } final ImageStreamListener streamListener = ImageStreamListener(listener); - if (maximumSize > 0 && maximumSizeBytes > 0) { - _pendingImages[key] = _PendingImage(result, streamListener); - } else { - untrackedPendingImage = _PendingImage(result, streamListener); + pendingImage = _PendingImage(result, streamListener); + if (trackPendingImage) { + _pendingImages[key] = pendingImage; } // Listener is removed in [_PendingImage.removeListener]. result.addListener(streamListener); diff --git a/packages/flutter/test/painting/image_cache_test.dart b/packages/flutter/test/painting/image_cache_test.dart index 13449fa19797..6e1542f53f99 100644 --- a/packages/flutter/test/painting/image_cache_test.dart +++ b/packages/flutter/test/painting/image_cache_test.dart @@ -332,6 +332,33 @@ void main() { expect(imageCache.liveImageCount, 0); }); + test('Clearing image cache does not leak live images', () async { + imageCache.maximumSize = 1; + + final ui.Image testImage1 = await createTestImage(width: 8, height: 8); + final ui.Image testImage2 = await createTestImage(width: 10, height: 10); + + final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); + final TestImageStreamCompleter completer2 = TestImageStreamCompleter()..testSetImage(testImage2); + + imageCache.putIfAbsent(testImage1, () => completer1); + expect(imageCache.statusForKey(testImage1).pending, true); + expect(imageCache.statusForKey(testImage1).live, true); + + imageCache.clear(); + expect(imageCache.statusForKey(testImage1).pending, false); + expect(imageCache.statusForKey(testImage1).live, true); + + completer1.testSetImage(testImage1); + expect(imageCache.statusForKey(testImage1).keepAlive, true); + expect(imageCache.statusForKey(testImage1).live, false); + + imageCache.putIfAbsent(testImage2, () => completer2); + expect(imageCache.statusForKey(testImage1).tracked, false); // evicted + expect(imageCache.statusForKey(testImage2).tracked, true); + }); + + test('Evicting a pending image clears the live image by default', () async { final ui.Image testImage = await createTestImage(width: 8, height: 8);