-
Notifications
You must be signed in to change notification settings - Fork 6k
Create an ImageHandle wrapper #21057
Changes from 5 commits
aa5caba
df403f3
6b700aa
8537a70
b69d226
0cf431c
4f15600
9eef907
17254e3
cb29ca9
fdaeb11
f00dd18
31d47de
6bd80bb
03324dc
084796f
bf4cfeb
e38a09d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1566,27 +1566,72 @@ enum PixelFormat { | |
| /// To draw an [Image], use one of the methods on the [Canvas] class, such as | ||
| /// [Canvas.drawImage]. | ||
| /// | ||
| /// A class or method that receives an image object must call [dispose] on the | ||
| /// handle when it is no longer needed. To create a shareable reference to the | ||
| /// underlying image, call [createHandle]. The method or object that recieves | ||
| /// the new instance will then be responsible for disposing it, and the | ||
| /// underlying image itself will be disposed when all outstanding handles are | ||
| /// disposed. | ||
| /// | ||
| /// If `dart:ui` passes an `Image` object and the recipient wishes to share | ||
| /// that handle with other callers, it is critical that [createHandle] is called | ||
| /// _before_ [dispose]. A handle that has been disposed cannot create new | ||
| /// handles anymore. | ||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * [Image](https://api.flutter.dev/flutter/widgets/Image-class.html), the class in the [widgets] library. | ||
| /// * [ImageDescriptor], which allows reading information about the image and | ||
| /// creating a codec to decode it. | ||
| /// * [instantiateImageCodec], a utility method that wraps [ImageDescriptor]. | ||
| /// | ||
| @pragma('vm:entry-point') | ||
| class Image extends NativeFieldWrapperClass2 { | ||
| // This class is created by the engine, and should not be instantiated | ||
| // or extended directly. | ||
| // | ||
| // To obtain an [Image] object, use [instantiateImageCodec]. | ||
| @pragma('vm:entry-point') | ||
| Image._(); | ||
| class Image { | ||
| Image._(this._image) { | ||
| assert(() { | ||
| _debugStack = StackTrace.current; | ||
| return true; | ||
| }()); | ||
| _image._handles.add(this); | ||
| } | ||
|
|
||
| final _Image _image; | ||
|
|
||
| StackTrace? _debugStack; | ||
|
|
||
| /// The number of image pixels along the image's horizontal axis. | ||
| int get width native 'Image_width'; | ||
| int get width { | ||
| assert(!_disposed && !_image._disposed); | ||
| return _image.width; | ||
| } | ||
|
|
||
| /// The number of image pixels along the image's vertical axis. | ||
| int get height native 'Image_height'; | ||
| int get height { | ||
| assert(!_disposed && !_image._disposed); | ||
| return _image.height; | ||
| } | ||
|
|
||
| /// Release this handle's claim on the underlying Image. This handle is no | ||
| /// longer usable after this method is called. | ||
| /// | ||
| /// Once all outstanding handles have been disposed, the underlying image will | ||
| /// be disposed as well. | ||
| /// | ||
| /// In debug mode, [debugGetOpenHandleStackTraces] will return a list of | ||
| /// [StackTrace] objects from all open handles' creation points. This is | ||
| /// useful when trying to determine what parts of the program are keeping an | ||
| /// image resident in memory. | ||
| void dispose() { | ||
| assert(() { | ||
| assert(!_disposed && !_image._disposed); | ||
| assert(_image._handles.contains(this)); | ||
| _disposed = true; | ||
| return true; | ||
| }()); | ||
| final bool removed = _image._handles.remove(this); | ||
| assert(removed); | ||
| if (_image._handles.isEmpty) { | ||
| _image.dispose(); | ||
| } | ||
| } | ||
|
|
||
| /// Converts the [Image] object into a byte array. | ||
| /// | ||
|
|
@@ -1595,6 +1640,56 @@ class Image extends NativeFieldWrapperClass2 { | |
| /// | ||
| /// Returns a future that completes with the binary image data or an error | ||
| /// if encoding fails. | ||
| Future<ByteData?> toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}) { | ||
| assert(!_disposed && !_image._disposed); | ||
| return _image.toByteData(format: format); | ||
| } | ||
|
|
||
| bool _disposed = false; | ||
|
||
|
|
||
| /// If asserts are enabled, returns the [StackTrace]s of each open handle from | ||
| /// [createHandle], in creation order. | ||
| /// | ||
| /// If asserts are disabled, this method always returns null. | ||
| List<StackTrace>? debugGetOpenHandleStackTraces() { | ||
| assert(!_disposed && !_image._disposed); | ||
| List<StackTrace>? stacks; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like there's an opportunity with this new API to avoid allowing nulls. How about initializing this to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we usually make the Is there an advantage to this? This method will never return a meaningful value in release.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there's already a convention around this, then sticking with that for this PR sgtm. The advantage is just avoiding mistakes around |
||
| assert(() { | ||
| stacks = _image._handles.map((Image handle) => handle._debugStack!).toList(); | ||
| return true; | ||
| }()); | ||
| return stacks; | ||
| } | ||
|
|
||
| /// Creates a disposable handle to this image. | ||
| /// | ||
| /// The returned object behaves identically to this image, except calling | ||
| /// [dispose] on it will only dispose the underlying native resources if it | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. docs need updating ("except" is no longer accurate)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
| /// is the last remaining handle. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add some additional details to describe when an additional handle should be created? And that it needs to be disposed to avoid leaks?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done and added a code sample. |
||
| Image createHandle() { | ||
|
||
| if (_disposed) { | ||
| throw StateError('Object disposed'); | ||
|
||
| } | ||
| return Image._(_image); | ||
| } | ||
|
|
||
| @override | ||
| String toString() => _image.toString(); | ||
| } | ||
|
|
||
| @pragma('vm:entry-point') | ||
| class _Image extends NativeFieldWrapperClass2 { | ||
| // This class is created by the engine, and should not be instantiated | ||
| // or extended directly. | ||
| // | ||
| // To obtain an [Image] object, use [instantiateImageCodec]. | ||
|
||
| @pragma('vm:entry-point') | ||
| _Image._(); | ||
|
|
||
| int get width native 'Image_width'; | ||
|
|
||
| int get height native 'Image_height'; | ||
|
|
||
| Future<ByteData?> toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}) { | ||
| return _futurize((_Callback<ByteData> callback) { | ||
| return _toByteData(format.index, (Uint8List? encoded) { | ||
|
|
@@ -1606,9 +1701,26 @@ class Image extends NativeFieldWrapperClass2 { | |
| /// Returns an error message on failure, null on success. | ||
| String? _toByteData(int format, _Callback<Uint8List?> callback) native 'Image_toByteData'; | ||
|
|
||
| /// Release the resources used by this object. The object is no longer usable | ||
| /// after this method is called. | ||
| void dispose() native 'Image_dispose'; | ||
| bool _disposed = false; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since this is only set in asserts, call it _debugDispose? |
||
| void dispose() { | ||
| assert(() { | ||
| assert(!_disposed); | ||
| assert( | ||
| _handles.isEmpty, | ||
| 'Attempted to dispose of an Image object that has ${_handles.length} ' | ||
| 'open handles.\n' | ||
| 'If you see this, it is a bug in dart:ui. Please file an issue at ' | ||
| 'https://github.com/flutter/flutter/issues/new.', | ||
| ); | ||
| _disposed = true; | ||
| return true; | ||
| }()); | ||
| _dispose(); | ||
| } | ||
|
|
||
| void _dispose() native 'Image_dispose'; | ||
|
|
||
| Set<Image> _handles = <Image>{}; | ||
|
|
||
| @override | ||
| String toString() => '[$width\u00D7$height]'; | ||
|
|
@@ -1635,8 +1747,11 @@ class FrameInfo extends NativeFieldWrapperClass2 { | |
| Duration get duration => Duration(milliseconds: _durationMillis); | ||
| int get _durationMillis native 'FrameInfo_durationMillis'; | ||
|
|
||
| Image? _cachedImage; | ||
|
|
||
| /// The [Image] object for this frame. | ||
| Image get image native 'FrameInfo_image'; | ||
| Image get image => _cachedImage ??= Image._(_image); | ||
| _Image get _image native 'FrameInfo_image'; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should definitely update the docs for FrameInfo, FrameInfo.image, and any APIs that return FrameInfo to say that the Image inside the returned object needs to be explicitly disposed and that if a handle to that image is passed it must first be cloned and that a handle to the FrameInfo itself must never be passed. ...which altogether sounds rather frightening. Should FrameInfo similarly be made cloneable to at least make it possible to create a new copy rather than making it a hot potato object? Having to dispose a property of a returned object is pretty weird as an API, I'm sure that would be a cause of leaks. Why is FrameInfo a NativeFieldWrapperClass2? It looks like it could just be created with a reference to _image and _durationMillis and be a pure Dart object.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only problem I can think of with making FrameInfo not a NativeFieldWrapperClass2 is that However, as implemented, I think if we do make it lazy, we don't have to tell people to dispose it unless they access it. I'm not a huge fan of cloning FrameInfo. You should either use the image and then dispose it, or you should pass it on without touching the image and tell the next person to dispose it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm. Making
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added some more documentation on |
||
|
|
||
| /// A handle to an image codec. | ||
|
|
@@ -3239,10 +3354,10 @@ class ImageShader extends Shader { | |
| if (matrix4.length != 16) | ||
| throw ArgumentError('"matrix4" must have 16 entries.'); | ||
| _constructor(); | ||
| _initWithImage(image, tmx.index, tmy.index, matrix4); | ||
| _initWithImage(image._image, tmx.index, tmy.index, matrix4); | ||
| } | ||
| void _constructor() native 'ImageShader_constructor'; | ||
| void _initWithImage(Image image, int tmx, int tmy, Float64List matrix4) native 'ImageShader_initWithImage'; | ||
| void _initWithImage(_Image image, int tmx, int tmy, Float64List matrix4) native 'ImageShader_initWithImage'; | ||
| } | ||
|
|
||
| /// Defines how a list of points is interpreted when drawing a set of triangles. | ||
|
|
@@ -3843,9 +3958,9 @@ class Canvas extends NativeFieldWrapperClass2 { | |
| assert(image != null); // image is checked on the engine side | ||
| assert(_offsetIsValid(offset)); | ||
| assert(paint != null); // ignore: unnecessary_null_comparison | ||
| _drawImage(image, offset.dx, offset.dy, paint._objects, paint._data); | ||
| _drawImage(image._image, offset.dx, offset.dy, paint._objects, paint._data); | ||
| } | ||
| void _drawImage(Image image, | ||
| void _drawImage(_Image image, | ||
| double x, | ||
| double y, | ||
| List<dynamic>? paintObjects, | ||
|
|
@@ -3866,7 +3981,7 @@ class Canvas extends NativeFieldWrapperClass2 { | |
| assert(_rectIsValid(src)); | ||
| assert(_rectIsValid(dst)); | ||
| assert(paint != null); // ignore: unnecessary_null_comparison | ||
| _drawImageRect(image, | ||
| _drawImageRect(image._image, | ||
| src.left, | ||
| src.top, | ||
| src.right, | ||
|
|
@@ -3878,7 +3993,7 @@ class Canvas extends NativeFieldWrapperClass2 { | |
| paint._objects, | ||
| paint._data); | ||
| } | ||
| void _drawImageRect(Image image, | ||
| void _drawImageRect(_Image image, | ||
| double srcLeft, | ||
| double srcTop, | ||
| double srcRight, | ||
|
|
@@ -3909,7 +4024,7 @@ class Canvas extends NativeFieldWrapperClass2 { | |
| assert(_rectIsValid(center)); | ||
| assert(_rectIsValid(dst)); | ||
| assert(paint != null); // ignore: unnecessary_null_comparison | ||
| _drawImageNine(image, | ||
| _drawImageNine(image._image, | ||
| center.left, | ||
| center.top, | ||
| center.right, | ||
|
|
@@ -3921,7 +4036,7 @@ class Canvas extends NativeFieldWrapperClass2 { | |
| paint._objects, | ||
| paint._data); | ||
| } | ||
| void _drawImageNine(Image image, | ||
| void _drawImageNine(_Image image, | ||
| double centerLeft, | ||
| double centerTop, | ||
| double centerRight, | ||
|
|
@@ -4198,7 +4313,7 @@ class Canvas extends NativeFieldWrapperClass2 { | |
| final Float32List? cullRectBuffer = cullRect?._value32; | ||
|
|
||
| _drawAtlas( | ||
| paint._objects, paint._data, atlas, rstTransformBuffer, rectBuffer, | ||
| paint._objects, paint._data, atlas._image, rstTransformBuffer, rectBuffer, | ||
| colorBuffer, (blendMode ?? BlendMode.src).index, cullRectBuffer | ||
| ); | ||
| } | ||
|
|
@@ -4367,14 +4482,14 @@ class Canvas extends NativeFieldWrapperClass2 { | |
| throw ArgumentError('If non-null, "colors" length must be one fourth the length of "rstTransforms" and "rects".'); | ||
|
|
||
| _drawAtlas( | ||
| paint._objects, paint._data, atlas, rstTransforms, rects, | ||
| paint._objects, paint._data, atlas._image, rstTransforms, rects, | ||
| colors, (blendMode ?? BlendMode.src).index, cullRect?._value32 | ||
| ); | ||
| } | ||
|
|
||
| void _drawAtlas(List<dynamic>? paintObjects, | ||
| ByteData paintData, | ||
| Image atlas, | ||
| _Image atlas, | ||
| Float32List rstTransforms, | ||
| Float32List rects, | ||
| Int32List? colors, | ||
|
|
@@ -4428,11 +4543,13 @@ class Picture extends NativeFieldWrapperClass2 { | |
| if (width <= 0 || height <= 0) | ||
| throw Exception('Invalid image dimensions.'); | ||
| return _futurize( | ||
| (_Callback<Image> callback) => _toImage(width, height, callback) | ||
| (_Callback<Image> callback) => _toImage(width, height, (_Image image) { | ||
| callback(Image._(image)); | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| String _toImage(int width, int height, _Callback<Image> callback) native 'Picture_toImage'; | ||
| String _toImage(int width, int height, _Callback<_Image> callback) native 'Picture_toImage'; | ||
|
|
||
| /// Release the resources used by this object. The object is no longer usable | ||
| /// after this method is called. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation on the method
toImageneeds updating explaining that you get a handle to an image that needs to be disposed when you're done with it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some more details here.