Skip to content

Commit b5adbee

Browse files
authored
Fix an issue that clearing the image cache may cause resource leaks (#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

Lines changed: 11 additions & 12 deletions
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

Lines changed: 27 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy