Skip to content

Commit b5adbee

Browse files
authored
Fix an issue that clearing the image cache may cause resource leaks (flutter#104527)
1 parent a56c5e5 commit b5adbee

File tree

2 files changed

+38
-12
lines changed

2 files changed

+38
-12
lines changed

packages/flutter/lib/src/painting/image_cache.dart

+11-12
Original file line numberDiff line numberDiff line change
@@ -397,16 +397,16 @@ class ImageCache {
397397
if (!kReleaseMode) {
398398
listenerTask = TimelineTask(parent: timelineTask)..start('listener');
399399
}
400-
// If we're doing tracing, we need to make sure that we don't try to finish
401-
// the trace entry multiple times if we get re-entrant calls from a multi-
402-
// frame provider here.
400+
// A multi-frame provider may call the listener more than once. We need do make
401+
// sure that some cleanup works won't run multiple times, such as finishing the
402+
// tracing task or removing the listeners
403403
bool listenedOnce = false;
404404

405405
// We shouldn't use the _pendingImages map if the cache is disabled, but we
406406
// will have to listen to the image at least once so we don't leak it in
407407
// the live image tracking.
408-
// If the cache is disabled, this variable will be set.
409-
_PendingImage? untrackedPendingImage;
408+
final bool trackPendingImage = maximumSize > 0 && maximumSizeBytes > 0;
409+
late _PendingImage pendingImage;
410410
void listener(ImageInfo? info, bool syncCall) {
411411
int? sizeBytes;
412412
if (info != null) {
@@ -421,14 +421,14 @@ class ImageCache {
421421
_trackLiveImage(key, result, sizeBytes);
422422

423423
// Only touch if the cache was enabled when resolve was initially called.
424-
if (untrackedPendingImage == null) {
424+
if (trackPendingImage) {
425425
_touch(key, image, listenerTask);
426426
} else {
427427
image.dispose();
428428
}
429429

430-
final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
431-
if (pendingImage != null) {
430+
_pendingImages.remove(key);
431+
if (!listenedOnce) {
432432
pendingImage.removeListener();
433433
}
434434
if (!kReleaseMode && !listenedOnce) {
@@ -445,10 +445,9 @@ class ImageCache {
445445
}
446446

447447
final ImageStreamListener streamListener = ImageStreamListener(listener);
448-
if (maximumSize > 0 && maximumSizeBytes > 0) {
449-
_pendingImages[key] = _PendingImage(result, streamListener);
450-
} else {
451-
untrackedPendingImage = _PendingImage(result, streamListener);
448+
pendingImage = _PendingImage(result, streamListener);
449+
if (trackPendingImage) {
450+
_pendingImages[key] = pendingImage;
452451
}
453452
// Listener is removed in [_PendingImage.removeListener].
454453
result.addListener(streamListener);

packages/flutter/test/painting/image_cache_test.dart

+27
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,33 @@ void main() {
332332
expect(imageCache.liveImageCount, 0);
333333
});
334334

335+
test('Clearing image cache does not leak live images', () async {
336+
imageCache.maximumSize = 1;
337+
338+
final ui.Image testImage1 = await createTestImage(width: 8, height: 8);
339+
final ui.Image testImage2 = await createTestImage(width: 10, height: 10);
340+
341+
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
342+
final TestImageStreamCompleter completer2 = TestImageStreamCompleter()..testSetImage(testImage2);
343+
344+
imageCache.putIfAbsent(testImage1, () => completer1);
345+
expect(imageCache.statusForKey(testImage1).pending, true);
346+
expect(imageCache.statusForKey(testImage1).live, true);
347+
348+
imageCache.clear();
349+
expect(imageCache.statusForKey(testImage1).pending, false);
350+
expect(imageCache.statusForKey(testImage1).live, true);
351+
352+
completer1.testSetImage(testImage1);
353+
expect(imageCache.statusForKey(testImage1).keepAlive, true);
354+
expect(imageCache.statusForKey(testImage1).live, false);
355+
356+
imageCache.putIfAbsent(testImage2, () => completer2);
357+
expect(imageCache.statusForKey(testImage1).tracked, false); // evicted
358+
expect(imageCache.statusForKey(testImage2).tracked, true);
359+
});
360+
361+
335362
test('Evicting a pending image clears the live image by default', () async {
336363
final ui.Image testImage = await createTestImage(width: 8, height: 8);
337364

0 commit comments

Comments
 (0)