Fix an issue that clearing the image cache may cause resource leaks (#104527)
diff --git a/packages/flutter/lib/src/painting/image_cache.dart b/packages/flutter/lib/src/painting/image_cache.dart
index f2068ae..f232714 100644
--- a/packages/flutter/lib/src/painting/image_cache.dart
+++ b/packages/flutter/lib/src/painting/image_cache.dart
@@ -397,16 +397,16 @@
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 @@
_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 @@
}
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 13449fa..6e1542f 100644
--- a/packages/flutter/test/painting/image_cache_test.dart
+++ b/packages/flutter/test/painting/image_cache_test.dart
@@ -332,6 +332,33 @@
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);