Unverified Commit 9c0337f3 authored by Dan Field's avatar Dan Field Committed by GitHub

Remove listeners for live images when we clear them (#51898)

* Remove listeners for live images when we clear them

* review, more test

* explanation
parent bd182e39
...@@ -82,7 +82,7 @@ class ImageCache { ...@@ -82,7 +82,7 @@ class ImageCache {
/// not fit into the _pendingImages or _cache objects. /// not fit into the _pendingImages or _cache objects.
/// ///
/// Unlike _cache, the [_CachedImage] for this may have a null byte size. /// Unlike _cache, the [_CachedImage] for this may have a null byte size.
final Map<Object, _CachedImage> _liveImages = <Object, _CachedImage>{}; final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{};
/// Maximum number of entries to store in the cache. /// Maximum number of entries to store in the cache.
/// ///
...@@ -237,7 +237,8 @@ class ImageCache { ...@@ -237,7 +237,8 @@ class ImageCache {
// will never complete, e.g. it was loaded in a FakeAsync zone. // will never complete, e.g. it was loaded in a FakeAsync zone.
// In such a case, we need to make sure subsequent calls to // In such a case, we need to make sure subsequent calls to
// putIfAbsent don't return this image that may never complete. // putIfAbsent don't return this image that may never complete.
_liveImages.remove(key); final _LiveImage image = _liveImages.remove(key);
image?.removeListener();
} }
final _PendingImage pendingImage = _pendingImages.remove(key); final _PendingImage pendingImage = _pendingImages.remove(key);
if (pendingImage != null) { if (pendingImage != null) {
...@@ -285,18 +286,17 @@ class ImageCache { ...@@ -285,18 +286,17 @@ class ImageCache {
} }
} }
void _trackLiveImage(Object key, _CachedImage image) { void _trackLiveImage(Object key, _LiveImage image, { bool debugPutOk = true }) {
// Avoid adding unnecessary callbacks to the completer. // Avoid adding unnecessary callbacks to the completer.
_liveImages.putIfAbsent(key, () { _liveImages.putIfAbsent(key, () {
assert(debugPutOk);
// Even if no callers to ImageProvider.resolve have listened to the stream, // Even if no callers to ImageProvider.resolve have listened to the stream,
// the cache is listening to the stream and will remove itself once the // the cache is listening to the stream and will remove itself once the
// image completes to move it from pending to keepAlive. // image completes to move it from pending to keepAlive.
// Even if the cache size is 0, we still add this listener. // Even if the cache size is 0, we still add this listener.
image.completer.addOnLastListenerRemovedCallback(() { image.completer.addOnLastListenerRemovedCallback(image.handleRemove);
_liveImages.remove(key);
});
return image; return image;
}); }).sizeBytes ??= image.sizeBytes;
} }
/// Returns the previously cached [ImageStream] for the given key, if available; /// Returns the previously cached [ImageStream] for the given key, if available;
...@@ -341,7 +341,7 @@ class ImageCache { ...@@ -341,7 +341,7 @@ class ImageCache {
} }
// The image might have been keptAlive but had no listeners (so not live). // The image might have been keptAlive but had no listeners (so not live).
// Make sure the cache starts tracking it as live again. // Make sure the cache starts tracking it as live again.
_trackLiveImage(key, image); _trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
_cache[key] = image; _cache[key] = image;
return image.completer; return image.completer;
} }
...@@ -357,7 +357,7 @@ class ImageCache { ...@@ -357,7 +357,7 @@ class ImageCache {
try { try {
result = loader(); result = loader();
_trackLiveImage(key, _CachedImage(result, null)); _trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
} catch (error, stackTrace) { } catch (error, stackTrace) {
if (!kReleaseMode) { if (!kReleaseMode) {
timelineTask.finish(arguments: <String, dynamic>{ timelineTask.finish(arguments: <String, dynamic>{
...@@ -392,13 +392,20 @@ class ImageCache { ...@@ -392,13 +392,20 @@ class ImageCache {
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4; final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result, imageSize); final _CachedImage image = _CachedImage(result, imageSize);
if (!_liveImages.containsKey(key)) {
assert(syncCall); _trackLiveImage(
result.addOnLastListenerRemovedCallback(() { key,
_liveImages.remove(key); _LiveImage(
}); result,
} imageSize,
_liveImages[key] = image; () => _liveImages.remove(key),
),
// This should result in a put if `loader()` above executed
// synchronously, in which case syncCall is true and we arrived here
// before we got a chance to track the image otherwise.
debugPutOk: syncCall,
);
final _PendingImage pendingImage = untrackedPendingImage ?? _pendingImages.remove(key); final _PendingImage pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
if (pendingImage != null) { if (pendingImage != null) {
pendingImage.removeListener(); pendingImage.removeListener();
...@@ -469,6 +476,9 @@ class ImageCache { ...@@ -469,6 +476,9 @@ class ImageCache {
/// memory pressure, since the live image caching only tracks image instances /// memory pressure, since the live image caching only tracks image instances
/// that are also being held by at least one other object. /// that are also being held by at least one other object.
void clearLiveImages() { void clearLiveImages() {
for (final _LiveImage image in _liveImages.values) {
image.removeListener();
}
_liveImages.clear(); _liveImages.clear();
} }
...@@ -577,7 +587,18 @@ class _CachedImage { ...@@ -577,7 +587,18 @@ class _CachedImage {
_CachedImage(this.completer, this.sizeBytes); _CachedImage(this.completer, this.sizeBytes);
final ImageStreamCompleter completer; final ImageStreamCompleter completer;
final int sizeBytes; int sizeBytes;
}
class _LiveImage extends _CachedImage {
_LiveImage(ImageStreamCompleter completer, int sizeBytes, this.handleRemove)
: super(completer, sizeBytes);
final VoidCallback handleRemove;
void removeListener() {
completer.removeOnLastListenerRemovedCallback(handleRemove);
}
} }
class _PendingImage { class _PendingImage {
......
...@@ -416,6 +416,13 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -416,6 +416,13 @@ abstract class ImageStreamCompleter extends Diagnosticable {
_onLastListenerRemovedCallbacks.add(callback); _onLastListenerRemovedCallbacks.add(callback);
} }
/// Removes a callback previously suppplied to
/// [addOnLastListenerRemovedCallback].
void removeOnLastListenerRemovedCallback(VoidCallback callback) {
assert(callback != null);
_onLastListenerRemovedCallbacks.remove(callback);
}
/// Calls all the registered listeners to notify them of a new image. /// Calls all the registered listeners to notify them of a new image.
@protected @protected
void setImage(ImageInfo image) { void setImage(ImageInfo image) {
......
...@@ -398,5 +398,86 @@ void main() { ...@@ -398,5 +398,86 @@ void main() {
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false); expect(imageCache.statusForKey(testImage).keepAlive, false);
}); });
test('Clearing liveImages removes callbacks', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(listener);
final TestImageStreamCompleter completer2 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(listener);
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
imageCache.clear();
imageCache.clearLiveImages();
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, false);
expect(imageCache.statusForKey(testImage).keepAlive, false);
imageCache.putIfAbsent(testImage, () => completer2);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
completer1.removeListener(listener);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
});
test('Live image gets size updated', () async {
// Add an image to the cache in pending state
// Complete it once it is in there as live
// Evict it but leave the live one.
// Add it again.
// If the live image did not track the size properly, the last line of
// this test will fail.
const TestImage testImage = TestImage(width: 8, height: 8);
const int testImageSize = 8 * 8 * 4;
final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..addListener(listener);
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
expect(imageCache.currentSizeBytes, 0);
completer1.testSetImage(testImage);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
expect(imageCache.currentSizeBytes, testImageSize);
imageCache.evict(testImage, includeLive: false);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
expect(imageCache.currentSizeBytes, 0);
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
expect(imageCache.currentSizeBytes, testImageSize);
});
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment