Remove listeners for live images when we clear them (#51898)
* Remove listeners for live images when we clear them * review, more test * explanation
This commit is contained in:
parent
bd182e3911
commit
9c0337f386
@ -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 {
|
|||||||
// 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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
// 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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
_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 {
|
|||||||
_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() {
|
|||||||
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user