Revert dispose images when done (#67100)
* Revert "docs for image disposal (#67066)"
This reverts commit bcb2ac5a55b90b2ccfd50de4684c0cbb8aca9290.
* Revert "Dispose of images after using them (#66688)"
This reverts commit a7954691dad4788f985cf17e81ce6c474a72e84e.
diff --git a/packages/flutter/lib/src/painting/decoration_image.dart b/packages/flutter/lib/src/painting/decoration_image.dart
index 60e151e..f257e53 100644
--- a/packages/flutter/lib/src/painting/decoration_image.dart
+++ b/packages/flutter/lib/src/painting/decoration_image.dart
@@ -295,11 +295,6 @@
void _handleImage(ImageInfo value, bool synchronousCall) {
if (_image == value)
return;
- if (_image != null && _image!.isCloneOf(value)) {
- value.dispose();
- return;
- }
- _image?.dispose();
_image = value;
assert(_onChanged != null);
if (!synchronousCall)
@@ -317,8 +312,6 @@
_handleImage,
onError: _details.onError,
));
- _image?.dispose();
- _image = null;
}
@override
@@ -436,12 +429,6 @@
assert(repeat != null);
assert(flipHorizontally != null);
assert(isAntiAlias != null);
- assert(
- image.debugGetOpenHandleStackTraces()?.isNotEmpty ?? true,
- 'Cannot paint an image that is disposed.\n'
- 'The caller of paintImage is expected to wait to dispose the image until '
- 'after painting has completed.'
- );
if (rect.isEmpty)
return;
Size outputSize = rect.size;
diff --git a/packages/flutter/lib/src/painting/image_cache.dart b/packages/flutter/lib/src/painting/image_cache.dart
index 2288415..28d68fa 100644
--- a/packages/flutter/lib/src/painting/image_cache.dart
+++ b/packages/flutter/lib/src/painting/image_cache.dart
@@ -6,7 +6,6 @@
import 'dart:ui' show hashValues;
import 'package:flutter/foundation.dart';
-import 'package:flutter/scheduler.dart';
import 'image_stream.dart';
@@ -239,7 +238,7 @@
// In such a case, we need to make sure subsequent calls to
// putIfAbsent don't return this image that may never complete.
final _LiveImage? image = _liveImages.remove(key);
- image?.dispose();
+ image?.removeListener();
}
final _PendingImage? pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
@@ -260,7 +259,6 @@
});
}
_currentSizeBytes -= image.sizeBytes!;
- image.dispose();
return true;
}
if (!kReleaseMode) {
@@ -278,30 +276,23 @@
/// [maximumSize] and [maximumSizeBytes].
void _touch(Object key, _CachedImage image, TimelineTask? timelineTask) {
assert(timelineTask != null);
- if (image.sizeBytes != null && image.sizeBytes! <= maximumSizeBytes && maximumSize > 0) {
+ if (image.sizeBytes != null && image.sizeBytes! <= maximumSizeBytes) {
_currentSizeBytes += image.sizeBytes!;
_cache[key] = image;
_checkCacheSize(timelineTask);
- } else {
- image.dispose();
}
}
- void _trackLiveImage(Object key, ImageStreamCompleter completer, int? sizeBytes) {
+ void _trackLiveImage(Object key, _LiveImage image) {
// Avoid adding unnecessary callbacks to the completer.
_liveImages.putIfAbsent(key, () {
// 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
// image completes to move it from pending to keepAlive.
- // Even if the cache size is 0, we still add this tracker, which will add
- // a keep alive handle to the stream.
- return _LiveImage(
- completer,
- () {
- _liveImages.remove(key);
- },
- );
- }).sizeBytes ??= sizeBytes;
+ // Even if the cache size is 0, we still add this listener.
+ image.completer.addOnLastListenerRemovedCallback(image.handleRemove);
+ return image;
+ }).sizeBytes ??= image.sizeBytes;
}
/// Returns the previously cached [ImageStream] for the given key, if available;
@@ -346,25 +337,14 @@
}
// The image might have been keptAlive but had no listeners (so not live).
// Make sure the cache starts tracking it as live again.
- _trackLiveImage(
- key,
- image.completer,
- image.sizeBytes,
- );
+ _trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
_cache[key] = image;
return image.completer;
}
- final _LiveImage? liveImage = _liveImages[key];
+ final _CachedImage? liveImage = _liveImages[key];
if (liveImage != null) {
- _touch(
- key,
- _CachedImage(
- liveImage.completer,
- sizeBytes: liveImage.sizeBytes,
- ),
- timelineTask,
- );
+ _touch(key, liveImage, timelineTask);
if (!kReleaseMode) {
timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
}
@@ -373,7 +353,7 @@
try {
result = loader();
- _trackLiveImage(key, result, null);
+ _trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
} catch (error, stackTrace) {
if (!kReleaseMode) {
timelineTask!.finish(arguments: <String, dynamic>{
@@ -404,33 +384,33 @@
// If the cache is disabled, this variable will be set.
_PendingImage? untrackedPendingImage;
void listener(ImageInfo? info, bool syncCall) {
- int? sizeBytes;
- if (info != null) {
- sizeBytes = info.image.height * info.image.width * 4;
- info.dispose();
- }
- final _CachedImage image = _CachedImage(
- result!,
- sizeBytes: sizeBytes,
+ // Images that fail to load don't contribute to cache size.
+ final int imageSize = info == null || info.image == null ? 0 : info.image.height * info.image.width * 4;
+
+ final _CachedImage image = _CachedImage(result!, imageSize);
+
+ _trackLiveImage(
+ key,
+ _LiveImage(
+ result,
+ imageSize,
+ () => _liveImages.remove(key),
+ ),
);
- _trackLiveImage(key, result, sizeBytes);
-
- // Only touch if the cache was enabled when resolve was initially called.
- if (untrackedPendingImage == null) {
- _touch(key, image, listenerTask);
- } else {
- image.dispose();
- }
-
final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
+ // Only touch if the cache was enabled when resolve was initially called.
+ if (untrackedPendingImage == null) {
+ _touch(key, image, listenerTask);
+ }
+
if (!kReleaseMode && !listenedOnce) {
listenerTask!.finish(arguments: <String, dynamic>{
'syncCall': syncCall,
- 'sizeInBytes': sizeBytes,
+ 'sizeInBytes': imageSize,
});
timelineTask!.finish(arguments: <String, dynamic>{
'currentSizeBytes': currentSizeBytes,
@@ -489,7 +469,7 @@
/// that are also being held by at least one other object.
void clearLiveImages() {
for (final _LiveImage image in _liveImages.values) {
- image.dispose();
+ image.removeListener();
}
_liveImages.clear();
}
@@ -509,7 +489,6 @@
final Object key = _cache.keys.first;
final _CachedImage image = _cache[key]!;
_currentSizeBytes -= image.sizeBytes!;
- image.dispose();
_cache.remove(key);
if (!kReleaseMode) {
finishArgs['evictedKeys'].add(key.toString());
@@ -597,59 +576,22 @@
String toString() => '${objectRuntimeType(this, 'ImageCacheStatus')}(pending: $pending, live: $live, keepAlive: $keepAlive)';
}
-/// Base class for [_CachedImage] and [_LiveImage].
-///
-/// Exists primarily so that a [_LiveImage] cannot be added to the
-/// [ImageCache._cache].
-abstract class _CachedImageBase {
- _CachedImageBase(
- this.completer, {
- this.sizeBytes,
- }) : assert(completer != null),
- handle = completer.keepAlive();
+class _CachedImage {
+ _CachedImage(this.completer, this.sizeBytes);
final ImageStreamCompleter completer;
int? sizeBytes;
- ImageStreamCompleterHandle? handle;
-
- @mustCallSuper
- void dispose() {
- assert(handle != null);
- // Give any interested parties a chance to listen to the stream before we
- // potentially dispose it.
- SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
- assert(handle != null);
- handle?.dispose();
- handle = null;
- });
- }
}
-class _CachedImage extends _CachedImageBase {
- _CachedImage(ImageStreamCompleter completer, {int? sizeBytes})
- : super(completer, sizeBytes: sizeBytes);
-}
+class _LiveImage extends _CachedImage {
+ _LiveImage(ImageStreamCompleter completer, int? sizeBytes, this.handleRemove)
+ : super(completer, sizeBytes);
-class _LiveImage extends _CachedImageBase {
- _LiveImage(ImageStreamCompleter completer, VoidCallback handleRemove, {int? sizeBytes})
- : super(completer, sizeBytes: sizeBytes) {
- _handleRemove = () {
- handleRemove();
- dispose();
- };
- completer.addOnLastListenerRemovedCallback(_handleRemove);
+ final VoidCallback handleRemove;
+
+ void removeListener() {
+ completer.removeOnLastListenerRemovedCallback(handleRemove);
}
-
- late VoidCallback _handleRemove;
-
- @override
- void dispose() {
- completer.removeOnLastListenerRemovedCallback(_handleRemove);
- super.dispose();
- }
-
- @override
- String toString() => describeIdentity(this);
}
class _PendingImage {
diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart
index 1c7b8bf..33167df 100644
--- a/packages/flutter/lib/src/painting/image_provider.dart
+++ b/packages/flutter/lib/src/painting/image_provider.dart
@@ -284,7 +284,6 @@
/// void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
/// setState(() {
/// // Trigger a build whenever the image changes.
-/// _imageInfo?.dispose();
/// _imageInfo = imageInfo;
/// });
/// }
@@ -292,8 +291,6 @@
/// @override
/// void dispose() {
/// _imageStream.removeListener(ImageStreamListener(_updateImage));
-/// _imageInfo?.dispose();
-/// _imageInfo = null;
/// super.dispose();
/// }
///
diff --git a/packages/flutter/lib/src/painting/image_stream.dart b/packages/flutter/lib/src/painting/image_stream.dart
index 069d683..670b677 100644
--- a/packages/flutter/lib/src/painting/image_stream.dart
+++ b/packages/flutter/lib/src/painting/image_stream.dart
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+
import 'dart:async';
import 'dart:ui' as ui show Image, Codec, FrameInfo;
import 'dart:ui' show hashValues;
@@ -13,84 +14,17 @@
///
/// ImageInfo objects are used by [ImageStream] objects to represent the
/// actual data of the image once it has been obtained.
-///
-/// The receiver of an [ImageInfo] object must call [dispose]. To safely share
-/// the object with other clients, use the [clone] method before calling
-/// dispose.
@immutable
class ImageInfo {
/// Creates an [ImageInfo] object for the given [image] and [scale].
///
- /// Both the [image] and the [scale] must not be null.
+ /// Both the image and the scale must not be null.
///
- /// The [debugLabel] may be used to identify the source of this image.
+ /// The tag may be used to identify the source of this image.
const ImageInfo({ required this.image, this.scale = 1.0, this.debugLabel })
: assert(image != null),
assert(scale != null);
- /// Creates an [ImageInfo] with a cloned [image].
- ///
- /// Once all outstanding references to the [image] are disposed, it is no
- /// longer safe to access properties of it or attempt to draw it. Clones serve
- /// to create new references to the underlying image data that can safely be
- /// disposed without knowledge of whether some other reference holder will
- /// still need access to the underlying image. Once a client disposes of its
- /// own image reference, it can no longer access the image, but other clients
- /// will be able to access their own references.
- ///
- /// This method must be used in cases where a client holding an [ImageInfo]
- /// needs to share the image info object with another client and will still
- /// need to access the underlying image data at some later point, e.g. to
- /// share it again with another client.
- ///
- /// See also:
- ///
- /// * [Image.clone], which describes how and why to clone images.
- ImageInfo clone() {
- return ImageInfo(
- image: image.clone(),
- scale: scale,
- debugLabel: debugLabel,
- );
- }
-
- /// Whether this [ImageInfo] is a [clone] of the `other`.
- ///
- /// This method is a convenience wrapper for [Image.isCloneOf], and is useful
- /// for clients that are trying to determine whether new layout or painting
- /// logic is required when recieving a new image reference.
- ///
- /// {@tool snippet}
- ///
- /// The following sample shows how to appropriately check whether the
- /// [ImageInfo] reference refers to new image data or not.
- ///
- /// ```dart
- /// ImageInfo _imageInfo;
- /// set imageInfo (ImageInfo value) {
- /// // If the image reference is exactly the same, do nothing.
- /// if (value == _imageInfo) {
- /// return;
- /// }
- /// // If it is a clone of the current reference, we must dispose of it and
- /// // can do so immediately. Since the underlying image has not changed,
- /// // We don't have any additional work to do here.
- /// if (value != null && _imageInfo != null && value.isCloneOf(_imageInfo)) {
- /// value.dispose();
- /// return;
- /// }
- /// _imageInfo?.dispose();
- /// _imageInfo = value;
- /// // Perform work to determine size, or paint the image.
- /// }
- /// ```
- /// {@end-tool}
- bool isCloneOf(ImageInfo other) {
- return other.image.isCloneOf(image)
- && scale == scale
- && other.debugLabel == debugLabel;
- }
-
/// The raw image pixels.
///
/// This is the object to pass to the [Canvas.drawImage],
@@ -112,15 +46,6 @@
/// A string used for debugging purpopses to identify the source of this image.
final String? debugLabel;
- /// Disposes of this object.
- ///
- /// Once this method has been called, the object should not be used anymore,
- /// and no clones of it or the image it contains can be made.
- void dispose() {
- assert((image.debugGetOpenHandleStackTraces()?.length ?? 1) > 0);
- image.dispose();
- }
-
@override
String toString() => '${debugLabel != null ? '$debugLabel ' : ''}$image @ ${debugFormatDouble(scale)}x';
@@ -212,10 +137,6 @@
///
/// Used in [ImageStreamListener].
///
-/// The `image` argument contains information about the image to be rendered.
-/// The implementer of [ImageStreamListener.onImage] is expected to call dispose
-/// on the [ui.Image] it receives.
-///
/// The `synchronousCall` argument is true if the listener is being invoked
/// during the call to `addListener`. This can be useful if, for example,
/// [ImageStream.addListener] is invoked during a frame, so that a new rendering
@@ -347,9 +268,6 @@
/// times when the image stream completes (whether because a new image is
/// available or because an error occurs). Likewise, to remove all instances
/// of the listener, [removeListener] would need to called N times as well.
- ///
- /// When a `listener` receives an [ImageInfo] object, the `listener` is
- /// responsible for disposing of the [ImageInfo.image].
/// {@endtemplate}
void addListener(ImageStreamListener listener) {
if (_completer != null)
@@ -407,37 +325,6 @@
}
}
-/// An opaque handle that keeps an [ImageStreamCompleter] alive even if it has
-/// lost its last listener.
-///
-/// To create a handle, use [ImageStreamCompleter.keepAlive].
-///
-/// Such handles are useful when an image cache needs to keep a completer alive
-/// but does not actually have a listener subscribed, or when a widget that
-/// displays an image needs to temporarily unsubscribe from the completer but
-/// may re-subscribe in the future, for example when the [TickerMode] changes.
-class ImageStreamCompleterHandle {
- ImageStreamCompleterHandle._(ImageStreamCompleter this._completer) {
- _completer!._keepAliveHandles += 1;
- }
-
- ImageStreamCompleter? _completer;
-
- /// Call this method to signal the [ImageStreamCompleter] that it can now be
- /// disposed when its last listener drops.
- ///
- /// This method must only be called once per object.
- void dispose() {
- assert(_completer != null);
- assert(_completer!._keepAliveHandles > 0);
- assert(!_completer!._disposed);
-
- _completer!._keepAliveHandles -= 1;
- _completer!._maybeDispose();
- _completer = null;
- }
-}
-
/// Base class for those that manage the loading of [dart:ui.Image] objects for
/// [ImageStream]s.
///
@@ -471,10 +358,6 @@
@visibleForTesting
bool get hasListeners => _listeners.isNotEmpty;
- /// We must avoid disposing a completer if it has never had a listener, even
- /// if all [keepAlive] handles get disposed.
- bool _hadAtLeastOneListener = false;
-
/// Adds a listener callback that is called whenever a new concrete [ImageInfo]
/// object is available or an error is reported. If a concrete image is
/// already available, or if an error has been already reported, this object
@@ -485,12 +368,10 @@
///
/// {@macro flutter.painting.imageStream.addListener}
void addListener(ImageStreamListener listener) {
- _checkDisposed();
- _hadAtLeastOneListener = true;
_listeners.add(listener);
if (_currentImage != null) {
try {
- listener.onImage(_currentImage!.clone(), true);
+ listener.onImage(_currentImage!, true);
} catch (exception, stack) {
reportError(
context: ErrorDescription('by a synchronously-called image listener'),
@@ -515,29 +396,11 @@
}
}
- int _keepAliveHandles = 0;
- /// Creates an [ImageStreamCompleterHandle] that will prevent this stream from
- /// being disposed at least until the handle is disposed.
- ///
- /// Such handles are useful when an image cache needs to keep a completer
- /// alive but does not itself have a listener subscribed, or when a widget
- /// that displays an image needs to temporarily unsubscribe from the completer
- /// but may re-subscribe in the future, for example when the [TickerMode]
- /// changes.
- ImageStreamCompleterHandle keepAlive() {
- _checkDisposed();
- return ImageStreamCompleterHandle._(this);
- }
-
/// Stops the specified [listener] from receiving image stream events.
///
/// If [listener] has been added multiple times, this removes the _first_
/// instance of the listener.
- ///
- /// Once all listeners have been removed and all [keepAlive] handles have been
- /// disposed, this image stream is no longer usable.
void removeListener(ImageStreamListener listener) {
- _checkDisposed();
for (int i = 0; i < _listeners.length; i += 1) {
if (_listeners[i] == listener) {
_listeners.removeAt(i);
@@ -545,49 +408,21 @@
}
}
if (_listeners.isEmpty) {
- final List<VoidCallback> callbacks = _onLastListenerRemovedCallbacks.toList();
- for (final VoidCallback callback in callbacks) {
+ for (final VoidCallback callback in _onLastListenerRemovedCallbacks) {
callback();
}
_onLastListenerRemovedCallbacks.clear();
- _maybeDispose();
- }
- }
-
- bool _disposed = false;
- void _maybeDispose() {
- if (!_hadAtLeastOneListener || _disposed || _listeners.isNotEmpty || _keepAliveHandles != 0) {
- return;
- }
-
- _currentImage?.dispose();
- _currentImage = null;
- _disposed = true;
- }
-
- void _checkDisposed() {
- if (_disposed) {
- throw StateError(
- 'Stream has been disposed.\n'
- 'An ImageStream is considered disposed once at least one listener has '
- 'been added and subsequently all listeners have been removed and no '
- 'handles are outstanding from the keepAlive method.\n'
- 'To resolve this error, maintain at least one listener on the stream, '
- 'or create an ImageStreamCompleterHandle from the keepAlive '
- 'method, or create a new stream for the image.',
- );
}
}
final List<VoidCallback> _onLastListenerRemovedCallbacks = <VoidCallback>[];
/// Adds a callback to call when [removeListener] results in an empty
- /// list of listeners and there are no [keepAlive] handles outstanding.
+ /// list of listeners.
///
/// This callback will never fire if [removeListener] is never called.
void addOnLastListenerRemovedCallback(VoidCallback callback) {
assert(callback != null);
- _checkDisposed();
_onLastListenerRemovedCallbacks.add(callback);
}
@@ -595,17 +430,13 @@
/// [addOnLastListenerRemovedCallback].
void removeOnLastListenerRemovedCallback(VoidCallback callback) {
assert(callback != null);
- _checkDisposed();
_onLastListenerRemovedCallbacks.remove(callback);
}
/// Calls all the registered listeners to notify them of a new image.
@protected
void setImage(ImageInfo image) {
- _checkDisposed();
- _currentImage?.dispose();
_currentImage = image;
-
if (_listeners.isEmpty)
return;
// Make a copy to allow for concurrent modification.
@@ -613,7 +444,7 @@
List<ImageStreamListener>.from(_listeners);
for (final ImageStreamListener listener in localListeners) {
try {
- listener.onImage(image.clone(), false);
+ listener.onImage(image, false);
} catch (exception, stack) {
reportError(
context: ErrorDescription('by an image listener'),
@@ -700,7 +531,6 @@
/// [ImageChunkEvent].
@protected
void reportImageChunkEvent(ImageChunkEvent event){
- _checkDisposed();
if (hasListeners) {
// Make a copy to allow for concurrent modification.
final List<ImageChunkListener> localListeners = _listeners
@@ -724,7 +554,6 @@
_listeners,
ifPresent: '${_listeners.length} listener${_listeners.length == 1 ? "" : "s" }',
));
- description.add(FlagProperty('disposed', value: _disposed, ifTrue: '<disposed>'));
}
}
@@ -871,16 +700,10 @@
_frameCallbackScheduled = false;
if (!hasListeners)
return;
- assert(_nextFrame != null);
if (_isFirstFrame() || _hasFrameDurationPassed(timestamp)) {
- _emitFrame(ImageInfo(
- image: _nextFrame!.image.clone(),
- scale: _scale,
- debugLabel: debugLabel,
- ));
+ _emitFrame(ImageInfo(image: _nextFrame!.image, scale: _scale, debugLabel: debugLabel));
_shownTimestamp = timestamp;
_frameDuration = _nextFrame!.duration;
- _nextFrame!.image.dispose();
_nextFrame = null;
final int completedCycles = _framesEmitted ~/ _codec!.frameCount;
if (_codec!.repetitionCount == -1 || completedCycles <= _codec!.repetitionCount) {
@@ -903,10 +726,6 @@
}
Future<void> _decodeNextFrameAndSchedule() async {
- // This will be null if we gave it away. If not, it's still ours and it
- // must be disposed of.
- _nextFrame?.image.dispose();
- _nextFrame = null;
try {
_nextFrame = await _codec!.getNextFrame();
} catch (exception, stack) {
@@ -922,13 +741,7 @@
if (_codec!.frameCount == 1) {
// This is not an animated image, just return it and don't schedule more
// frames.
- _emitFrame(ImageInfo(
- image: _nextFrame!.image.clone(),
- scale: _scale,
- debugLabel: debugLabel,
- ));
- _nextFrame!.image.dispose();
- _nextFrame = null;
+ _emitFrame(ImageInfo(image: _nextFrame!.image, scale: _scale, debugLabel: debugLabel));
return;
}
_scheduleAppFrame();
diff --git a/packages/flutter/lib/src/rendering/image.dart b/packages/flutter/lib/src/rendering/image.dart
index 7832f61..2659f69 100644
--- a/packages/flutter/lib/src/rendering/image.dart
+++ b/packages/flutter/lib/src/rendering/image.dart
@@ -85,16 +85,8 @@
ui.Image? get image => _image;
ui.Image? _image;
set image(ui.Image? value) {
- if (value == _image) {
+ if (value == _image)
return;
- }
- // If we get a clone of our image, it's the same underlying native data -
- // dispose of the new clone and return early.
- if (value != null && _image != null && value.isCloneOf(_image!)) {
- value.dispose();
- return;
- }
- _image?.dispose();
_image = value;
markNeedsPaint();
if (_width == null || _height == null)
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index 2958280..5c3d80b 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -5363,10 +5363,6 @@
/// The image is painted using [paintImage], which describes the meanings of the
/// various fields on this class in more detail.
///
-/// The [image] is not disposed of by this widget. Creators of the widget are
-/// expected to call [Image.dispose] on the [image] once the [RawImage] is no
-/// longer buildable.
-///
/// This widget is rarely used directly. Instead, consider using [Image].
class RawImage extends LeafRenderObjectWidget {
/// Creates a widget that displays an image.
@@ -5398,10 +5394,6 @@
super(key: key);
/// The image to display.
- ///
- /// Since a [RawImage] is stateless, it does not ever dispose this image.
- /// Creators of a [RawImage] are expected to call [Image.dispose] on this
- /// image handle when the [RawImage] will no longer be needed.
final ui.Image? image;
/// A string identifying the source of the image.
@@ -5524,13 +5516,8 @@
@override
RenderImage createRenderObject(BuildContext context) {
assert((!matchTextDirection && alignment is Alignment) || debugCheckHasDirectionality(context));
- assert(
- image?.debugGetOpenHandleStackTraces()?.isNotEmpty ?? true,
- 'Creator of a RawImage disposed of the image when the RawImage still '
- 'needed it.'
- );
return RenderImage(
- image: image?.clone(),
+ image: image,
debugImageLabel: debugImageLabel,
width: width,
height: height,
@@ -5551,13 +5538,8 @@
@override
void updateRenderObject(BuildContext context, RenderImage renderObject) {
- assert(
- image?.debugGetOpenHandleStackTraces()?.isNotEmpty ?? true,
- 'Creator of a RawImage disposed of the image when the RawImage still '
- 'needed it.'
- );
renderObject
- ..image = image?.clone()
+ ..image = image
..debugImageLabel = debugImageLabel
..width = width
..height = height
@@ -5575,12 +5557,6 @@
}
@override
- void didUnmountRenderObject(RenderImage renderObject) {
- // Have the render object dispose its image handle.
- renderObject.image = null;
- }
-
- @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ui.Image>('image', image));
diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart
index 4a30bd3..0011bf5 100644
--- a/packages/flutter/lib/src/widgets/image.dart
+++ b/packages/flutter/lib/src/widgets/image.dart
@@ -1084,7 +1084,6 @@
late DisposableBuildContext<State<Image>> _scrollAwareContext;
Object? _lastException;
StackTrace? _lastStack;
- ImageStreamCompleterHandle? _completerHandle;
@override
void initState() {
@@ -1098,9 +1097,7 @@
assert(_imageStream != null);
WidgetsBinding.instance!.removeObserver(this);
_stopListeningToStream();
- _completerHandle?.dispose();
_scrollAwareContext.dispose();
- _replaceImage(info: null);
super.dispose();
}
@@ -1112,7 +1109,7 @@
if (TickerMode.of(context))
_listenToStream();
else
- _stopListeningToStream(keepStreamAlive: true);
+ _stopListeningToStream();
super.didChangeDependencies();
}
@@ -1122,9 +1119,8 @@
super.didUpdateWidget(oldWidget);
if (_isListeningToStream &&
(widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) {
- final ImageStreamListener oldListener = _getListener();
+ _imageStream!.removeListener(_getListener());
_imageStream!.addListener(_getListener(recreateListener: true));
- _imageStream!.removeListener(oldListener);
}
if (widget.image != oldWidget.image)
_resolveImage();
@@ -1186,7 +1182,7 @@
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
- _replaceImage(info: imageInfo);
+ _imageInfo = imageInfo;
_loadingProgress = null;
_lastException = null;
_lastStack = null;
@@ -1204,11 +1200,6 @@
});
}
- void _replaceImage({required ImageInfo? info}) {
- _imageInfo?.dispose();
- _imageInfo = info;
- }
-
// Updates _imageStream to newStream, and moves the stream listener
// registration from the old stream to the new stream (if a listener was
// registered).
@@ -1220,7 +1211,7 @@
_imageStream!.removeListener(_getListener());
if (!widget.gaplessPlayback)
- setState(() { _replaceImage(info: null); });
+ setState(() { _imageInfo = null; });
setState(() {
_loadingProgress = null;
@@ -1236,29 +1227,13 @@
void _listenToStream() {
if (_isListeningToStream)
return;
-
_imageStream!.addListener(_getListener());
- _completerHandle?.dispose();
- _completerHandle = null;
-
_isListeningToStream = true;
}
- /// Stops listening to the image stream, if this state object has attached a
- /// listener.
- ///
- /// If the listener from this state is the last listener on the stream, the
- /// stream will be disposed. To keep the stream alive, set `keepStreamAlive`
- /// to true, which create [ImageStreamCompleterHandle] to keep the completer
- /// alive and is compatible with the [TickerMode] being off.
- void _stopListeningToStream({bool keepStreamAlive = false}) {
+ void _stopListeningToStream() {
if (!_isListeningToStream)
return;
-
- if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) {
- _completerHandle = _imageStream!.completer!.keepAlive();
- }
-
_imageStream!.removeListener(_getListener());
_isListeningToStream = false;
}
@@ -1271,10 +1246,6 @@
}
Widget result = RawImage(
- // Do not clone the image, because RawImage is a stateless wrapper.
- // The image will be disposed by this state object when it is not needed
- // anymore, such as when it is unmounted or when the image stream pushes
- // a new image.
image: _imageInfo?.image,
debugImageLabel: _imageInfo?.debugLabel,
width: widget.width,
diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart
index 060fc68..a4161f0 100644
--- a/packages/flutter/test/painting/decoration_test.dart
+++ b/packages/flutter/test/painting/decoration_test.dart
@@ -6,7 +6,6 @@
@TestOn('!chrome')
import 'dart:async';
-import 'dart:typed_data';
import 'dart:ui' as ui show Image, ColorFilter;
import 'package:flutter/foundation.dart';
@@ -14,7 +13,6 @@
import 'package:fake_async/fake_async.dart';
import '../flutter_test_alternative.dart';
-import '../image_data.dart';
import '../painting/mocks_for_image_cache.dart';
import '../rendering/rendering_tester.dart';
@@ -64,10 +62,6 @@
}
class AsyncTestImageProvider extends ImageProvider<int> {
- AsyncTestImageProvider(this.image);
-
- final ui.Image image;
-
@override
Future<int> obtainKey(ImageConfiguration configuration) {
return Future<int>.value(2);
@@ -76,7 +70,7 @@
@override
ImageStreamCompleter load(int key, DecoderCallback decode) {
return OneFrameImageStreamCompleter(
- Future<ImageInfo>.value(TestImageInfo(key, image: image))
+ Future<ImageInfo>.value(TestImageInfo(key))
);
}
}
@@ -106,31 +100,6 @@
String toString() => '${describeIdentity(this)}()';
}
-class MultiFrameImageProvider extends ImageProvider<MultiFrameImageProvider> {
- MultiFrameImageProvider(this.completer);
-
- final MultiImageCompleter completer;
-
- @override
- Future<MultiFrameImageProvider> obtainKey(ImageConfiguration configuration) {
- return SynchronousFuture<MultiFrameImageProvider>(this);
- }
-
- @override
- ImageStreamCompleter load(MultiFrameImageProvider key, DecoderCallback decode) {
- return completer;
- }
-
- @override
- String toString() => '${describeIdentity(this)}()';
-}
-
-class MultiImageCompleter extends ImageStreamCompleter {
- void testSetImage(ImageInfo info) {
- setImage(info);
- }
-}
-
void main() {
TestRenderingFlutterBinding(); // initializes the imageCache
@@ -182,10 +151,9 @@
expect(onChangedCalled, equals(false));
});
- test('BoxDecorationImageListenerAsync', () async {
- final ui.Image image = await createTestImage(width: 10, height: 10);
+ test('BoxDecorationImageListenerAsync', () {
FakeAsync().run((FakeAsync async) {
- final ImageProvider imageProvider = AsyncTestImageProvider(image);
+ final ImageProvider imageProvider = AsyncTestImageProvider();
final DecorationImage backgroundImage = DecorationImage(image: imageProvider);
final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
@@ -205,42 +173,6 @@
});
});
- test('BoxDecorationImageListener does not change when image is clone', () async {
- final ui.Image image1 = await createTestImage(width: 10, height: 10, cache: false);
- final ui.Image image2 = await createTestImage(width: 10, height: 10, cache: false);
- final MultiImageCompleter completer = MultiImageCompleter();
- final MultiFrameImageProvider imageProvider = MultiFrameImageProvider(completer);
- final DecorationImage backgroundImage = DecorationImage(image: imageProvider);
-
- final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
- bool onChangedCalled = false;
- final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
- onChangedCalled = true;
- });
-
- final TestCanvas canvas = TestCanvas();
- const ImageConfiguration imageConfiguration = ImageConfiguration(size: Size.zero);
- boxPainter.paint(canvas, Offset.zero, imageConfiguration);
-
- // The onChanged callback should be invoked asynchronously.
- expect(onChangedCalled, equals(false));
-
- completer.testSetImage(ImageInfo(image: image1.clone()));
- await null;
-
- expect(onChangedCalled, equals(true));
- onChangedCalled = false;
- completer.testSetImage(ImageInfo(image: image1.clone()));
- await null;
-
- expect(onChangedCalled, equals(false));
-
- completer.testSetImage(ImageInfo(image: image2.clone()));
- await null;
-
- expect(onChangedCalled, equals(true));
- });
-
// Regression test for https://github.com/flutter/flutter/issues/7289.
// A reference test would be better.
test('BoxDecoration backgroundImage clip', () async {
@@ -694,30 +626,4 @@
expect(call.positionalArguments[2].size, const Size(25.0, 25.0));
expect(call.positionalArguments[2], const Rect.fromLTRB(0.0, 0.0, 25.0, 25.0));
});
-
- test('DecorationImagePainter disposes of image when disposed', () async {
- final ImageProvider provider = MemoryImage(Uint8List.fromList(kTransparentImage));
-
- final ImageStream stream = provider.resolve(ImageConfiguration.empty);
-
- final Completer<ImageInfo> infoCompleter = Completer<ImageInfo>();
- void _listener(ImageInfo image, bool syncCall) {
- assert(!infoCompleter.isCompleted);
- infoCompleter.complete(image);
- }
- stream.addListener(ImageStreamListener(_listener));
-
- final ImageInfo info = await infoCompleter.future;
- final int baselineRefCount = info.image.debugGetOpenHandleStackTraces().length;
-
- final DecorationImagePainter painter = DecorationImage(image: provider).createPainter(() {});
- final Canvas canvas = TestCanvas();
- painter.paint(canvas, Rect.zero, Path(), ImageConfiguration.empty);
-
- expect(info.image.debugGetOpenHandleStackTraces().length, baselineRefCount + 1);
- painter.dispose();
- expect(info.image.debugGetOpenHandleStackTraces().length, baselineRefCount);
-
- info.dispose();
- });
}
diff --git a/packages/flutter/test/painting/image_cache_test.dart b/packages/flutter/test/painting/image_cache_test.dart
index 718642f..d5af602 100644
--- a/packages/flutter/test/painting/image_cache_test.dart
+++ b/packages/flutter/test/painting/image_cache_test.dart
@@ -6,11 +6,9 @@
import 'dart:ui' as ui;
-import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
-import 'package:flutter/scheduler.dart';
-
import '../flutter_test_alternative.dart';
+
import '../rendering/rendering_tester.dart';
import 'mocks_for_image_cache.dart';
@@ -458,6 +456,7 @@
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..addListener(listener);
+
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
@@ -485,88 +484,4 @@
expect(imageCache.statusForKey(testImage).keepAlive, true);
expect(imageCache.currentSizeBytes, testImageSize);
});
-
- test('Image is obtained and disposed of properly for cache', () async {
- const int key = 1;
- final ui.Image testImage = await createTestImage(width: 8, height: 8, cache: false);
- expect(testImage.debugGetOpenHandleStackTraces().length, 1);
-
- ImageInfo imageInfo;
- final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {
- imageInfo = info;
- });
-
- final TestImageStreamCompleter completer = TestImageStreamCompleter();
-
- completer.addListener(listener);
- imageCache.putIfAbsent(key, () => completer);
-
- expect(testImage.debugGetOpenHandleStackTraces().length, 1);
-
- // This should cause keepAlive to be set to true.
- completer.testSetImage(testImage);
- expect(imageInfo, isNotNull);
- // +1 ImageStreamCompleter
- expect(testImage.debugGetOpenHandleStackTraces().length, 2);
-
- completer.removeListener(listener);
-
- // Force us to the end of the frame.
- SchedulerBinding.instance.scheduleFrame();
- await SchedulerBinding.instance.endOfFrame;
-
- expect(testImage.debugGetOpenHandleStackTraces().length, 2);
-
- expect(imageCache.evict(key), true);
-
- // Force us to the end of the frame.
- SchedulerBinding.instance.scheduleFrame();
- await SchedulerBinding.instance.endOfFrame;
-
- // -1 _CachedImage
- // -1 ImageStreamCompleter
- expect(testImage.debugGetOpenHandleStackTraces().length, 1);
-
- imageInfo.dispose();
- expect(testImage.debugGetOpenHandleStackTraces().length, 0);
- }, skip: kIsWeb); // Web does not care about image handles.
-
- test('Image is obtained and disposed of properly for cache when listener is still active', () async {
- const int key = 1;
- final ui.Image testImage = await createTestImage(width: 8, height: 8, cache: false);
- expect(testImage.debugGetOpenHandleStackTraces().length, 1);
-
- ImageInfo imageInfo;
- final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {
- imageInfo = info;
- });
-
- final TestImageStreamCompleter completer = TestImageStreamCompleter();
-
- completer.addListener(listener);
- imageCache.putIfAbsent(key, () => completer);
-
- expect(testImage.debugGetOpenHandleStackTraces().length, 1);
-
- // This should cause keepAlive to be set to true.
- completer.testSetImage(testImage);
- expect(imageInfo, isNotNull);
- // Just our imageInfo and the completer.
- expect(testImage.debugGetOpenHandleStackTraces().length, 2);
-
- expect(imageCache.evict(key), true);
-
- // Force us to the end of the frame.
- SchedulerBinding.instance.scheduleFrame();
- await SchedulerBinding.instance.endOfFrame;
-
- // Live image still around since there's still a listener, and the listener
- // should be holding a handle.
- expect(testImage.debugGetOpenHandleStackTraces().length, 2);
- completer.removeListener(listener);
-
- expect(testImage.debugGetOpenHandleStackTraces().length, 1);
- imageInfo.dispose();
- expect(testImage.debugGetOpenHandleStackTraces().length, 0);
- }, skip: kIsWeb); // Web does not care about open image handles.
}
diff --git a/packages/flutter/test/painting/image_stream_test.dart b/packages/flutter/test/painting/image_stream_test.dart
index 3f156d9..2f9aa75 100644
--- a/packages/flutter/test/painting/image_stream_test.dart
+++ b/packages/flutter/test/painting/image_stream_test.dart
@@ -8,12 +8,13 @@
import 'dart:ui';
import 'package:flutter/painting.dart';
-import 'package:flutter/scheduler.dart' show timeDilation, SchedulerBinding;
+import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart';
class FakeFrameInfo implements FrameInfo {
- const FakeFrameInfo(this._duration, this._image);
+ FakeFrameInfo(this._duration, this._image);
+
final Duration _duration;
final Image _image;
@@ -23,15 +24,6 @@
@override
Image get image => _image;
-
- int get imageHandleCount => image.debugGetOpenHandleStackTraces().length;
-
- FakeFrameInfo clone() {
- return FakeFrameInfo(
- _duration,
- _image.clone(),
- );
- }
}
class MockCodec implements Codec {
@@ -80,7 +72,7 @@
void main() {
Image image20x10;
Image image200x100;
- setUp(() async {
+ setUpAll(() async {
image20x10 = await createTestImage(width: 20, height: 10);
image200x100 = await createTestImage(width: 200, height: 100);
});
@@ -307,7 +299,7 @@
mockCodec.completeNextFrame(frame);
await tester.idle();
- expect(emittedImages.every((ImageInfo info) => info.image.isCloneOf(frame.image)), true);
+ expect(emittedImages, equals(<ImageInfo>[ImageInfo(image: frame.image)]));
});
testWidgets('ImageStream emits frames (animated images)', (WidgetTester tester) async {
@@ -337,7 +329,7 @@
expect(emittedImages.length, 0);
await tester.pump();
- expect(emittedImages.single.image.isCloneOf(frame1.image), true);
+ expect(emittedImages, equals(<ImageInfo>[ImageInfo(image: frame1.image)]));
final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
mockCodec.completeNextFrame(frame2);
@@ -348,8 +340,10 @@
expect(emittedImages.length, 1);
await tester.pump(const Duration(milliseconds: 100));
- expect(emittedImages[0].image.isCloneOf(frame1.image), true);
- expect(emittedImages[1].image.isCloneOf(frame2.image), true);
+ expect(emittedImages, equals(<ImageInfo>[
+ ImageInfo(image: frame1.image),
+ ImageInfo(image: frame2.image),
+ ]));
// Let the pending timer for the next frame to complete so we can cleanly
// quit the test without pending timers.
@@ -375,22 +369,24 @@
codecCompleter.complete(mockCodec);
await tester.idle();
- final FakeFrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
- final FakeFrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
+ final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
+ final FrameInfo frame2 = FakeFrameInfo(const Duration(milliseconds: 400), image200x100);
- mockCodec.completeNextFrame(frame1.clone());
+ mockCodec.completeNextFrame(frame1);
await tester.idle(); // let nextFrameFuture complete
await tester.pump(); // first animation frame shows on first app frame.
- mockCodec.completeNextFrame(frame2.clone());
+ mockCodec.completeNextFrame(frame2);
await tester.idle(); // let nextFrameFuture complete
await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
- mockCodec.completeNextFrame(frame1.clone());
+ mockCodec.completeNextFrame(frame1);
await tester.idle(); // let nextFrameFuture complete
await tester.pump(const Duration(milliseconds: 400)); // emit 3rd frame
- expect(emittedImages[0].image.isCloneOf(frame1.image), true);
- expect(emittedImages[1].image.isCloneOf(frame2.image), true);
- expect(emittedImages[2].image.isCloneOf(frame1.image), true);
+ expect(emittedImages, equals(<ImageInfo>[
+ ImageInfo(image: frame1.image),
+ ImageInfo(image: frame2.image),
+ ImageInfo(image: frame1.image),
+ ]));
// Let the pending timer for the next frame to complete so we can cleanly
// quit the test without pending timers.
@@ -431,11 +427,13 @@
await tester.idle();
await tester.pump(const Duration(milliseconds: 400));
- expect(emittedImages[0].image.isCloneOf(frame1.image), true);
- expect(emittedImages[1].image.isCloneOf(frame2.image), true);
+ expect(emittedImages, equals(<ImageInfo>[
+ ImageInfo(image: frame1.image),
+ ImageInfo(image: frame2.image),
+ ]));
});
- testWidgets('frames are only decoded when there are listeners', (WidgetTester tester) async {
+ testWidgets('frames are only decoded when there are active listeners', (WidgetTester tester) async {
final MockCodec mockCodec = MockCodec();
mockCodec.frameCount = 2;
mockCodec.repetitionCount = -1;
@@ -448,7 +446,6 @@
final ImageListener listener = (ImageInfo image, bool synchronousCall) { };
imageStream.addListener(ImageStreamListener(listener));
- final ImageStreamCompleterHandle handle = imageStream.keepAlive();
codecCompleter.complete(mockCodec);
await tester.idle();
@@ -471,8 +468,6 @@
imageStream.addListener(ImageStreamListener(listener));
await tester.idle(); // let nextFrameFuture complete
expect(mockCodec.numFramesAsked, 3);
-
- handle.dispose();
});
testWidgets('multiple stream listeners', (WidgetTester tester) async {
@@ -506,9 +501,8 @@
mockCodec.completeNextFrame(frame1);
await tester.idle(); // let nextFrameFuture complete
await tester.pump(); // first animation frame shows on first app frame.
-
- expect(emittedImages1.single.image.isCloneOf(frame1.image), true);
- expect(emittedImages2.single.image.isCloneOf(frame1.image), true);
+ expect(emittedImages1, equals(<ImageInfo>[ImageInfo(image: frame1.image)]));
+ expect(emittedImages2, equals(<ImageInfo>[ImageInfo(image: frame1.image)]));
mockCodec.completeNextFrame(frame2);
await tester.idle(); // let nextFrameFuture complete
@@ -516,10 +510,11 @@
imageStream.removeListener(ImageStreamListener(listener1));
await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.
- expect(emittedImages1.single.image.isCloneOf(frame1.image), true);
- expect(emittedImages2[0].image.isCloneOf(frame1.image), true);
- expect(emittedImages2[1].image.isCloneOf(frame2.image), true);
-
+ expect(emittedImages1, equals(<ImageInfo>[ImageInfo(image: frame1.image)]));
+ expect(emittedImages2, equals(<ImageInfo>[
+ ImageInfo(image: frame1.image),
+ ImageInfo(image: frame2.image),
+ ]));
});
testWidgets('timer is canceled when listeners are removed', (WidgetTester tester) async {
@@ -644,8 +639,8 @@
await tester.idle(); // let nextFrameFuture complete
- imageStream.addListener(ImageStreamListener(listener));
imageStream.removeListener(ImageStreamListener(listener));
+ imageStream.addListener(ImageStreamListener(listener));
final FrameInfo frame1 = FakeFrameInfo(const Duration(milliseconds: 200), image20x10);
@@ -689,76 +684,6 @@
compare(onImage1: handleImage, onChunk1: handleChunk, onError1: handleError, onImage2: handleImage, onError2: handleError, areEqual: false);
});
- testWidgets('Keep alive handles do not drive frames or prevent last listener callbacks', (WidgetTester tester) async {
- final Image image10x10 = await tester.runAsync(() => createTestImage(width: 10, height: 10));
- final MockCodec mockCodec = MockCodec();
- mockCodec.frameCount = 2;
- mockCodec.repetitionCount = -1;
- final Completer<Codec> codecCompleter = Completer<Codec>();
-
- final ImageStreamCompleter imageStream = MultiFrameImageStreamCompleter(
- codec: codecCompleter.future,
- scale: 1.0,
- );
-
- int onImageCount = 0;
- final ImageListener activeListener = (ImageInfo image, bool synchronousCall) {
- onImageCount += 1;
- };
- bool lastListenerDropped = false;
- imageStream.addOnLastListenerRemovedCallback(() {
- lastListenerDropped = true;
- });
-
- expect(lastListenerDropped, false);
- final ImageStreamCompleterHandle handle = imageStream.keepAlive();
- expect(lastListenerDropped, false);
- SchedulerBinding.instance.debugAssertNoTransientCallbacks('Only passive listeners');
-
- codecCompleter.complete(mockCodec);
- await tester.idle();
-
- expect(onImageCount, 0);
-
- final FakeFrameInfo frame1 = FakeFrameInfo(Duration.zero, image20x10);
- mockCodec.completeNextFrame(frame1);
- await tester.idle();
- SchedulerBinding.instance.debugAssertNoTransientCallbacks('Only passive listeners');
- await tester.pump();
- expect(onImageCount, 0);
-
- imageStream.addListener(ImageStreamListener(activeListener));
-
- final FakeFrameInfo frame2 = FakeFrameInfo(Duration.zero, image10x10);
- mockCodec.completeNextFrame(frame2);
- await tester.idle();
- expect(SchedulerBinding.instance.transientCallbackCount, 1);
- await tester.pump();
-
- expect(onImageCount, 1);
-
- imageStream.removeListener(ImageStreamListener(activeListener));
- expect(lastListenerDropped, true);
-
- mockCodec.completeNextFrame(frame1);
- await tester.idle();
- expect(SchedulerBinding.instance.transientCallbackCount, 1);
- await tester.pump();
-
- expect(onImageCount, 1);
-
- SchedulerBinding.instance.debugAssertNoTransientCallbacks('Only passive listeners');
-
- mockCodec.completeNextFrame(frame2);
- await tester.idle();
- SchedulerBinding.instance.debugAssertNoTransientCallbacks('Only passive listeners');
- await tester.pump();
-
- expect(onImageCount, 1);
-
- handle.dispose();
- });
-
// TODO(amirh): enable this once WidgetTester supports flushTimers.
// https://github.com/flutter/flutter/issues/30344
// testWidgets('remove and add listener before a delayed frame is scheduled', (WidgetTester tester) async {
diff --git a/packages/flutter/test/painting/mocks_for_image_cache.dart b/packages/flutter/test/painting/mocks_for_image_cache.dart
index c8ca57f..899254c 100644
--- a/packages/flutter/test/painting/mocks_for_image_cache.dart
+++ b/packages/flutter/test/painting/mocks_for_image_cache.dart
@@ -10,10 +10,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
-// ignore: must_be_immutable
class TestImageInfo implements ImageInfo {
- TestImageInfo(this.value, { @required this.image, this.scale = 1.0, this.debugLabel })
- : assert(image != null);
+ const TestImageInfo(this.value, { this.image, this.scale = 1.0, this.debugLabel });
@override
final ui.Image image;
@@ -28,39 +26,6 @@
@override
String toString() => '$runtimeType($value)';
-
- @override
- TestImageInfo clone() {
- return TestImageInfo(value, image: image.clone(), scale: scale, debugLabel: debugLabel);
- }
-
- @override
- bool isCloneOf(ImageInfo other) {
- assert(other != null);
- return other.image.isCloneOf(image)
- && scale == scale
- && other.debugLabel == debugLabel;
- }
-
- @override
- void dispose() {
- image.dispose();
- }
-
- @override
- int get hashCode => hashValues(value, image, scale, debugLabel);
-
- @override
- bool operator ==(Object other) {
- if (other.runtimeType != runtimeType)
- return false;
- return other is TestImageInfo
- && other.value == value
- && other.image.isCloneOf(image)
- && other.scale == scale
- && other.debugLabel == debugLabel;
-
- }
}
class TestImageProvider extends ImageProvider<int> {
@@ -79,7 +44,7 @@
@override
ImageStreamCompleter load(int key, DecoderCallback decode) {
return OneFrameImageStreamCompleter(
- SynchronousFuture<ImageInfo>(TestImageInfo(imageValue, image: image.clone()))
+ SynchronousFuture<ImageInfo>(TestImageInfo(imageValue, image: image))
);
}
diff --git a/packages/flutter/test/rendering/image_test.dart b/packages/flutter/test/rendering/image_test.dart
index f06377b..504d44b 100644
--- a/packages/flutter/test/rendering/image_test.dart
+++ b/packages/flutter/test/rendering/image_test.dart
@@ -176,38 +176,4 @@
image.colorBlendMode = BlendMode.color;
expect(image.colorBlendMode, BlendMode.color);
});
-
- test('Render image disposes its image', () async {
- final ui.Image image = await createTestImage(width: 10, height: 10, cache: false);
- expect(image.debugGetOpenHandleStackTraces().length, 1);
-
- final RenderImage renderImage = RenderImage(image: image.clone());
- expect(image.debugGetOpenHandleStackTraces().length, 2);
-
- renderImage.image = image.clone();
- expect(image.debugGetOpenHandleStackTraces().length, 2);
-
- renderImage.image = null;
- expect(image.debugGetOpenHandleStackTraces().length, 1);
-
- image.dispose();
- expect(image.debugGetOpenHandleStackTraces().length, 0);
- }, skip: kIsWeb); // Web doesn't track open image handles.
-
- test('Render image does not dispose its image if setting the same image twice', () async {
- final ui.Image image = await createTestImage(width: 10, height: 10, cache: false);
- expect(image.debugGetOpenHandleStackTraces().length, 1);
-
- final RenderImage renderImage = RenderImage(image: image.clone());
- expect(image.debugGetOpenHandleStackTraces().length, 2);
-
- renderImage.image = renderImage.image;
- expect(image.debugGetOpenHandleStackTraces().length, 2);
-
- renderImage.image = null;
- expect(image.debugGetOpenHandleStackTraces().length, 1);
-
- image.dispose();
- expect(image.debugGetOpenHandleStackTraces().length, 0);
- }, skip: kIsWeb); // Web doesn't track open image handles.
}
diff --git a/packages/flutter/test/rendering/mock_canvas.dart b/packages/flutter/test/rendering/mock_canvas.dart
index 934d79f..9aa9c59 100644
--- a/packages/flutter/test/rendering/mock_canvas.dart
+++ b/packages/flutter/test/rendering/mock_canvas.dart
@@ -1315,7 +1315,7 @@
void verifyArguments(List<dynamic> arguments) {
super.verifyArguments(arguments);
final ui.Image imageArgument = arguments[0] as ui.Image;
- if (image != null && !image.isCloneOf(imageArgument))
+ if (image != null && imageArgument != image)
throw 'It called $methodName with an image, $imageArgument, which was not exactly the expected image ($image).';
final Offset pointArgument = arguments[0] as Offset;
if (x != null && y != null) {
@@ -1359,7 +1359,7 @@
void verifyArguments(List<dynamic> arguments) {
super.verifyArguments(arguments);
final ui.Image imageArgument = arguments[0] as ui.Image;
- if (image != null && !image.isCloneOf(imageArgument))
+ if (image != null && imageArgument != image)
throw 'It called $methodName with an image, $imageArgument, which was not exactly the expected image ($image).';
final Rect sourceArgument = arguments[1] as Rect;
if (source != null && sourceArgument != source)
diff --git a/packages/flutter/test/widgets/fade_in_image_test.dart b/packages/flutter/test/widgets/fade_in_image_test.dart
index 371879e..4277938 100644
--- a/packages/flutter/test/widgets/fade_in_image_test.dart
+++ b/packages/flutter/test/widgets/fade_in_image_test.dart
@@ -132,15 +132,15 @@
placeholderProvider.complete();
await tester.pump();
- expect(findFadeInImage(tester).placeholder.rawImage.image.isCloneOf(placeholderImage), true);
+ expect(findFadeInImage(tester).placeholder.rawImage.image, same(placeholderImage));
expect(findFadeInImage(tester).target.rawImage.image, null);
imageProvider.complete();
await tester.pump();
for (int i = 0; i < 5; i += 1) {
final FadeInImageParts parts = findFadeInImage(tester);
- expect(parts.placeholder.rawImage.image.isCloneOf(placeholderImage), true);
- expect(parts.target.rawImage.image.isCloneOf(targetImage), true);
+ expect(parts.placeholder.rawImage.image, same(placeholderImage));
+ expect(parts.target.rawImage.image, same(targetImage));
expect(parts.placeholder.opacity, moreOrLessEquals(1 - i / 5));
expect(parts.target.opacity, 0);
await tester.pump(const Duration(milliseconds: 10));
@@ -148,8 +148,8 @@
for (int i = 0; i < 5; i += 1) {
final FadeInImageParts parts = findFadeInImage(tester);
- expect(parts.placeholder.rawImage.image.isCloneOf(placeholderImage), true);
- expect(parts.target.rawImage.image.isCloneOf(targetImage), true);
+ expect(parts.placeholder.rawImage.image, same(placeholderImage));
+ expect(parts.target.rawImage.image, same(targetImage));
expect(parts.placeholder.opacity, 0);
expect(parts.target.opacity, moreOrLessEquals(i / 5));
await tester.pump(const Duration(milliseconds: 10));
@@ -159,7 +159,7 @@
placeholder: placeholderProvider,
image: imageProvider,
));
- expect(findFadeInImage(tester).target.rawImage.image.isCloneOf(targetImage), true);
+ expect(findFadeInImage(tester).target.rawImage.image, same(targetImage));
expect(findFadeInImage(tester).target.opacity, 1);
});
@@ -174,7 +174,7 @@
image: imageProvider,
));
- expect(findFadeInImage(tester).target.rawImage.image.isCloneOf(targetImage), true);
+ expect(findFadeInImage(tester).target.rawImage.image, same(targetImage));
expect(findFadeInImage(tester).placeholder, isNull);
expect(findFadeInImage(tester).target.opacity, 1);
});
@@ -195,7 +195,7 @@
final State state = findFadeInImage(tester).state;
placeholderProvider.complete();
await tester.pump();
- expect(findFadeInImage(tester).placeholder.rawImage.image.isCloneOf(placeholderImage), true);
+ expect(findFadeInImage(tester).placeholder.rawImage.image, same(placeholderImage));
await tester.pumpWidget(FadeInImage(
placeholder: secondPlaceholderProvider,
@@ -207,7 +207,7 @@
secondPlaceholderProvider.complete();
await tester.pump();
- expect(findFadeInImage(tester).placeholder.rawImage.image.isCloneOf(replacementImage), true);
+ expect(findFadeInImage(tester).placeholder.rawImage.image, same(replacementImage));
expect(findFadeInImage(tester).state, same(state));
});
@@ -263,7 +263,7 @@
secondImageProvider.complete();
await tester.pump();
- expect(findFadeInImage(tester).target.rawImage.image.isCloneOf(replacementImage), true);
+ expect(findFadeInImage(tester).target.rawImage.image, same(replacementImage));
expect(findFadeInImage(tester).state, same(state));
expect(findFadeInImage(tester).placeholder.opacity, moreOrLessEquals(1));
expect(findFadeInImage(tester).target.opacity, moreOrLessEquals(0));
diff --git a/packages/flutter/test/widgets/image_test.dart b/packages/flutter/test/widgets/image_test.dart
index 7cb2507..6cb3cce 100644
--- a/packages/flutter/test/widgets/image_test.dart
+++ b/packages/flutter/test/widgets/image_test.dart
@@ -472,7 +472,7 @@
await tester.pump();
expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 1 listener), pixels: $imageString @ 1.0x, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)'));
await tester.pumpWidget(Container());
- expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(lifecycle state: defunct, not mounted, stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 0 listeners), pixels: null, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)'));
+ expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(lifecycle state: defunct, not mounted, stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, $imageString @ 1.0x, 0 listeners), pixels: $imageString @ 1.0x, loadingProgress: null, frameNumber: 0, wasSynchronouslyLoaded: false)'));
});
testWidgets('Stream completer errors can be listened to by attaching before resolving', (WidgetTester tester) async {
@@ -904,8 +904,8 @@
});
testWidgets('Image State can be reconfigured to use another image', (WidgetTester tester) async {
- final Image image1 = Image(image: TestImageProvider()..complete(image10x10.clone()), width: 10.0, excludeFromSemantics: true);
- final Image image2 = Image(image: TestImageProvider()..complete(image10x10.clone()), width: 20.0, excludeFromSemantics: true);
+ final Image image1 = Image(image: TestImageProvider()..complete(image10x10), width: 10.0, excludeFromSemantics: true);
+ final Image image2 = Image(image: TestImageProvider()..complete(image10x10), width: 20.0, excludeFromSemantics: true);
final Column column = Column(children: <Widget>[image1, image2]);
await tester.pumpWidget(column, null, EnginePhase.layout);
@@ -1039,7 +1039,7 @@
});
testWidgets('Image invokes frameBuilder with correct wasSynchronouslyLoaded=true', (WidgetTester tester) async {
- final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(ImageInfo(image: image10x10.clone()));
+ final TestImageStreamCompleter streamCompleter = TestImageStreamCompleter(ImageInfo(image: image10x10));
final TestImageProvider imageProvider = TestImageProvider(streamCompleter: streamCompleter);
int lastFrame;
bool lastFrameWasSync;
@@ -1058,7 +1058,7 @@
expect(lastFrame, 0);
expect(lastFrameWasSync, isTrue);
expect(find.byType(RawImage), findsOneWidget);
- streamCompleter.setData(imageInfo: ImageInfo(image: image10x10.clone()));
+ streamCompleter.setData(imageInfo: ImageInfo(image: image10x10));
await tester.pump();
expect(lastFrame, 1);
expect(lastFrameWasSync, isTrue);
@@ -1474,10 +1474,10 @@
expect(provider1.loadCallCount, 1);
expect(provider2.loadCallCount, 1);
- provider1.complete(image10x10.clone());
+ provider1.complete(image10x10);
await tester.idle();
- provider2.complete(image10x10.clone());
+ provider2.complete(image10x10);
await tester.idle();
expect(imageCache.liveImageCount, 2);
@@ -1763,76 +1763,6 @@
debugOnPaintImage = null;
});
- testWidgets('Disposes image handle when disposed', (WidgetTester tester) async {
- final ui.Image image = await tester.runAsync(() => createTestImage(width: 1, height: 1, cache: false));
-
- expect(image.debugGetOpenHandleStackTraces().length, 1);
-
- final ImageProvider provider = TestImageProvider(
- streamCompleter: OneFrameImageStreamCompleter(
- Future<ImageInfo>.value(
- ImageInfo(
- image: image,
- scale: 1.0,
- debugLabel: 'TestImage',
- ),
- ),
- ),
- );
-
- // creating the provider should not have changed anything, and the provider
- // now owns the handle.
- expect(image.debugGetOpenHandleStackTraces().length, 1);
-
- await tester.pumpWidget(Image(image: provider));
-
- // Image widget + 1, render object + 1
- expect(image.debugGetOpenHandleStackTraces().length, 3);
-
- await tester.pumpWidget(const SizedBox());
-
- // Image widget and render object go away
- expect(image.debugGetOpenHandleStackTraces().length, 1);
-
- await provider.evict();
-
- tester.binding.scheduleFrame();
- await tester.pump();
-
- // Image cache listener go away and Image stream listeners go away.
- // Image is now at zero.
- expect(image.debugGetOpenHandleStackTraces().length, 0);
- }, skip: kIsWeb); // Web does not care about image handle/disposal.
-
- testWidgets('Keeps stream alive when ticker mode is disabled', (WidgetTester tester) async {
- imageCache.maximumSize = 0;
- final ui.Image image = await tester.runAsync(() => createTestImage(width: 1, height: 1, cache: false));
- final TestImageProvider provider = TestImageProvider();
- provider.complete(image);
-
- await tester.pumpWidget(
- TickerMode(
- enabled: true,
- child: Image(image: provider),
- ),
- );
- expect(find.byType(Image), findsOneWidget);
-
- await tester.pumpWidget(TickerMode(
- enabled: false,
- child: Image(image: provider),
- ),
- );
- expect(find.byType(Image), findsOneWidget);
-
- await tester.pumpWidget(TickerMode(
- enabled: true,
- child: Image(image: provider),
- ),
- );
- expect(find.byType(Image), findsOneWidget);
- });
-
testWidgets('Load a good image after a bad image was loaded should not call errorBuilder', (WidgetTester tester) async {
final UniqueKey errorKey = UniqueKey();
final ui.Image image = await tester.runAsync(() => createTestImage());
@@ -1969,12 +1899,6 @@
String toString() => '${describeIdentity(this)}()';
}
-class SimpleTestImageStreamCompleter extends ImageStreamCompleter {
- void testSetImage(ui.Image image) {
- setImage(ImageInfo(image: image, scale: 1.0));
- }
-}
-
class TestImageStreamCompleter extends ImageStreamCompleter {
TestImageStreamCompleter([this._currentImage]);
@@ -1985,7 +1909,7 @@
void addListener(ImageStreamListener listener) {
listeners.add(listener);
if (_currentImage != null) {
- listener.onImage(_currentImage.clone(), true);
+ listener.onImage(_currentImage, true);
}
}
@@ -1999,13 +1923,12 @@
ImageChunkEvent chunkEvent,
}) {
if (imageInfo != null) {
- _currentImage?.dispose();
_currentImage = imageInfo;
}
final List<ImageStreamListener> localListeners = listeners.toList();
for (final ImageStreamListener listener in localListeners) {
if (imageInfo != null) {
- listener.onImage(imageInfo.clone(), false);
+ listener.onImage(imageInfo, false);
}
if (chunkEvent != null && listener.onChunk != null) {
listener.onChunk(chunkEvent);
diff --git a/packages/flutter_test/lib/src/animation_sheet.dart b/packages/flutter_test/lib/src/animation_sheet.dart
index 274c2b6..4dbb82f 100644
--- a/packages/flutter_test/lib/src/animation_sheet.dart
+++ b/packages/flutter_test/lib/src/animation_sheet.dart
@@ -166,7 +166,7 @@
key: key,
cellSize: frameSize,
children: frames.map((ui.Image image) => RawImage(
- image: image.clone(),
+ image: image,
width: frameSize.width,
height: frameSize.height,
)).toList(),
diff --git a/packages/flutter_test/lib/src/image.dart b/packages/flutter_test/lib/src/image.dart
index 038ed0f..3bfcc1e 100644
--- a/packages/flutter_test/lib/src/image.dart
+++ b/packages/flutter_test/lib/src/image.dart
@@ -38,12 +38,12 @@
final int cacheKey = hashValues(width, height);
if (cache && _cache.containsKey(cacheKey)) {
- return _cache[cacheKey]!.clone();
+ return _cache[cacheKey]!;
}
final ui.Image image = await _createImage(width, height);
if (cache) {
- _cache[cacheKey] = image.clone();
+ _cache[cacheKey] = image;
}
return image;
});