Skip to content

Commit 35a0b9f

Browse files
authored
Refactor CanvasKit image ref counting; fix a minor memory leak (flutter#22549)
* Refactor SkiaObjectBox ref counting * make CkAnimatedImage a Codec * disallow double dispose; better assertion messages
1 parent 9a5fd32 commit 35a0b9f

File tree

6 files changed

+249
-214
lines changed

6 files changed

+249
-214
lines changed

lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,14 +685,11 @@ class SkAnimatedImage {
685685
external SkImage getCurrentFrame();
686686
external int width();
687687
external int height();
688-
external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY);
689-
external SkData encodeToData();
690688

691689
/// Deletes the C++ object.
692690
///
693691
/// This object is no longer usable after calling this method.
694692
external void delete();
695-
external bool isAliasOf(SkAnimatedImage other);
696693
external bool isDeleted();
697694
}
698695

@@ -1820,6 +1817,7 @@ class SkData {
18201817
external int size();
18211818
external bool isEmpty();
18221819
external Uint8List bytes();
1820+
external void delete();
18231821
}
18241822

18251823
@JS()

lib/web_ui/lib/src/engine/canvaskit/image.dart

Lines changed: 96 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,17 @@ part of engine;
88
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia.
99
void skiaInstantiateImageCodec(Uint8List list, Callback<ui.Codec> callback,
1010
[int? width, int? height, int? format, int? rowBytes]) {
11-
final SkAnimatedImage skAnimatedImage =
12-
canvasKit.MakeAnimatedImageFromEncoded(list);
13-
final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage);
14-
final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage);
11+
final CkAnimatedImage codec = CkAnimatedImage.decodeFromBytes(list);
1512
callback(codec);
1613
}
1714

1815
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after
1916
/// requesting from URI.
2017
Future<ui.Codec> skiaInstantiateWebImageCodec(
21-
String src, WebOnlyImageCodecChunkCallback? chunkCallback) {
18+
String uri, WebOnlyImageCodecChunkCallback? chunkCallback) {
2219
Completer<ui.Codec> completer = Completer<ui.Codec>();
2320
//TODO: Switch to using MakeImageFromCanvasImageSource when animated images are supported.
24-
html.HttpRequest.request(src, responseType: "arraybuffer",
21+
html.HttpRequest.request(uri, responseType: "arraybuffer",
2522
onProgress: (html.ProgressEvent event) {
2623
if (event.lengthComputable) {
2724
chunkCallback?.call(event.loaded!, event.total!);
@@ -33,134 +30,123 @@ Future<ui.Codec> skiaInstantiateWebImageCodec(
3330
}
3431
final Uint8List list =
3532
new Uint8List.view((response.response as ByteBuffer));
36-
final SkAnimatedImage skAnimatedImage =
37-
canvasKit.MakeAnimatedImageFromEncoded(list);
38-
final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage);
39-
final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage);
33+
final CkAnimatedImage codec = CkAnimatedImage.decodeFromBytes(list);
4034
completer.complete(codec);
4135
}, onError: (dynamic error) {
4236
completer.completeError(error);
4337
});
4438
return completer.future;
4539
}
4640

47-
/// A wrapper for `SkAnimatedImage`.
48-
class CkAnimatedImage implements ui.Image {
49-
// Use a box because `SkImage` may be deleted either due to this object
50-
// being garbage-collected, or by an explicit call to [delete].
51-
late final SkiaObjectBox<SkAnimatedImage> box;
41+
/// The CanvasKit implementation of [ui.Codec].
42+
///
43+
/// Wraps `SkAnimatedImage`.
44+
class CkAnimatedImage implements ui.Codec, StackTraceDebugger {
45+
/// Decodes an image from a list of encoded bytes.
46+
CkAnimatedImage.decodeFromBytes(Uint8List bytes) {
47+
if (assertionsEnabled) {
48+
_debugStackTrace = StackTrace.current;
49+
}
50+
final SkAnimatedImage skAnimatedImage =
51+
canvasKit.MakeAnimatedImageFromEncoded(bytes);
52+
box = SkiaObjectBox<CkAnimatedImage, SkAnimatedImage>(this, skAnimatedImage);
53+
}
5254

53-
SkAnimatedImage get _skAnimatedImage => box.skObject;
55+
// Use a box because `CkAnimatedImage` may be deleted either due to this
56+
// object being garbage-collected, or by an explicit call to [dispose].
57+
late final SkiaObjectBox<CkAnimatedImage, SkAnimatedImage> box;
5458

55-
CkAnimatedImage(SkAnimatedImage skAnimatedImage) {
56-
box = SkiaObjectBox<SkAnimatedImage>(this, skAnimatedImage);
57-
}
59+
@override
60+
StackTrace get debugStackTrace => _debugStackTrace!;
61+
StackTrace? _debugStackTrace;
62+
63+
bool _disposed = false;
64+
bool get debugDisposed => _disposed;
5865

59-
CkAnimatedImage.cloneOf(SkiaObjectBox<SkAnimatedImage> boxToClone) {
60-
box = boxToClone.clone(this);
66+
bool _debugCheckIsNotDisposed() {
67+
assert(!_disposed, 'This image has been disposed.');
68+
return true;
6169
}
6270

63-
bool _disposed = false;
6471
@override
6572
void dispose() {
66-
box.delete();
73+
assert(
74+
!_disposed,
75+
'Cannot dispose a codec that has already been disposed.',
76+
);
6777
_disposed = true;
78+
79+
// This image is no longer usable. Bump the ref count.
80+
box.unref(this);
6881
}
6982

7083
@override
71-
bool get debugDisposed {
72-
if (assertionsEnabled) {
73-
return _disposed;
74-
}
75-
throw StateError(
76-
'Image.debugDisposed is only available when asserts are enabled.');
84+
int get frameCount {
85+
assert(_debugCheckIsNotDisposed());
86+
return box.skiaObject.getFrameCount();
7787
}
7888

79-
ui.Image clone() => CkAnimatedImage.cloneOf(box);
80-
8189
@override
82-
bool isCloneOf(ui.Image other) {
83-
return other is CkAnimatedImage &&
84-
other._skAnimatedImage.isAliasOf(_skAnimatedImage);
90+
int get repetitionCount {
91+
assert(_debugCheckIsNotDisposed());
92+
return box.skiaObject.getRepetitionCount();
8593
}
8694

8795
@override
88-
List<StackTrace>? debugGetOpenHandleStackTraces() =>
89-
box.debugGetStackTraces();
90-
91-
int get frameCount => _skAnimatedImage.getFrameCount();
92-
93-
/// Decodes the next frame and returns the frame duration.
94-
Duration decodeNextFrame() {
95-
final int durationMillis = _skAnimatedImage.decodeNextFrame();
96-
return Duration(milliseconds: durationMillis);
96+
Future<ui.FrameInfo> getNextFrame() {
97+
assert(_debugCheckIsNotDisposed());
98+
final int durationMillis = box.skiaObject.decodeNextFrame();
99+
final Duration duration = Duration(milliseconds: durationMillis);
100+
final CkImage image = CkImage(box.skiaObject.getCurrentFrame());
101+
return Future<ui.FrameInfo>.value(AnimatedImageFrameInfo(duration, image));
97102
}
103+
}
98104

99-
int get repetitionCount => _skAnimatedImage.getRepetitionCount();
100-
101-
CkImage get currentFrameAsImage {
102-
return CkImage(_skAnimatedImage.getCurrentFrame());
105+
/// A [ui.Image] backed by an `SkImage` from Skia.
106+
class CkImage implements ui.Image, StackTraceDebugger {
107+
CkImage(SkImage skImage) {
108+
if (assertionsEnabled) {
109+
_debugStackTrace = StackTrace.current;
110+
}
111+
box = SkiaObjectBox<CkImage, SkImage>(this, skImage);
103112
}
104113

105-
@override
106-
int get width => _skAnimatedImage.width();
107-
108-
@override
109-
int get height => _skAnimatedImage.height();
110-
111-
@override
112-
Future<ByteData> toByteData(
113-
{ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) {
114-
Uint8List bytes;
115-
116-
if (format == ui.ImageByteFormat.rawRgba) {
117-
final SkImageInfo imageInfo = SkImageInfo(
118-
alphaType: canvasKit.AlphaType.Premul,
119-
colorType: canvasKit.ColorType.RGBA_8888,
120-
colorSpace: SkColorSpaceSRGB,
121-
width: width,
122-
height: height,
123-
);
124-
bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0);
125-
} else {
126-
// Defaults to PNG 100%.
127-
final SkData skData = _skAnimatedImage.encodeToData();
128-
// Make a copy that we can return.
129-
bytes = Uint8List.fromList(canvasKit.getDataBytes(skData));
114+
CkImage.cloneOf(this.box) {
115+
if (assertionsEnabled) {
116+
_debugStackTrace = StackTrace.current;
130117
}
131-
132-
final ByteData data = bytes.buffer.asByteData(0, bytes.length);
133-
return Future<ByteData>.value(data);
118+
box.ref(this);
134119
}
135120

136121
@override
137-
String toString() => '[$width\u00D7$height]';
138-
}
122+
StackTrace get debugStackTrace => _debugStackTrace!;
123+
StackTrace? _debugStackTrace;
139124

140-
/// A [ui.Image] backed by an `SkImage` from Skia.
141-
class CkImage implements ui.Image {
142125
// Use a box because `SkImage` may be deleted either due to this object
143126
// being garbage-collected, or by an explicit call to [delete].
144-
late final SkiaObjectBox<SkImage> box;
127+
late final SkiaObjectBox<CkImage, SkImage> box;
145128

146-
SkImage get skImage => box.skObject;
129+
/// The underlying Skia image object.
130+
///
131+
/// Do not store the returned value. It is memory-managed by [SkiaObjectBox].
132+
/// Storing it may result in use-after-free bugs.
133+
SkImage get skImage => box.skiaObject;
147134

148-
CkImage(SkImage skImage) {
149-
box = SkiaObjectBox<SkImage>(this, skImage);
150-
}
135+
bool _disposed = false;
151136

152-
CkImage.cloneOf(SkiaObjectBox<SkImage> boxToClone) {
153-
box = boxToClone.clone(this);
137+
bool _debugCheckIsNotDisposed() {
138+
assert(!_disposed, 'This image has been disposed.');
139+
return true;
154140
}
155141

156-
bool _disposed = false;
157142
@override
158143
void dispose() {
159-
box.delete();
160-
assert(() {
161-
_disposed = true;
162-
return true;
163-
}());
144+
assert(
145+
!_disposed,
146+
'Cannot dispose an image that has already been disposed.',
147+
);
148+
_disposed = true;
149+
box.unref(this);
164150
}
165151

166152
@override
@@ -173,10 +159,14 @@ class CkImage implements ui.Image {
173159
}
174160

175161
@override
176-
ui.Image clone() => CkImage.cloneOf(box);
162+
ui.Image clone() {
163+
assert(_debugCheckIsNotDisposed());
164+
return CkImage.cloneOf(box);
165+
}
177166

178167
@override
179168
bool isCloneOf(ui.Image other) {
169+
assert(_debugCheckIsNotDisposed());
180170
return other is CkImage && other.skImage.isAliasOf(skImage);
181171
}
182172

@@ -185,14 +175,21 @@ class CkImage implements ui.Image {
185175
box.debugGetStackTraces();
186176

187177
@override
188-
int get width => skImage.width();
178+
int get width {
179+
assert(_debugCheckIsNotDisposed());
180+
return skImage.width();
181+
}
189182

190183
@override
191-
int get height => skImage.height();
184+
int get height {
185+
assert(_debugCheckIsNotDisposed());
186+
return skImage.height();
187+
}
192188

193189
@override
194190
Future<ByteData> toByteData(
195191
{ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) {
192+
assert(_debugCheckIsNotDisposed());
196193
Uint8List bytes;
197194

198195
if (format == ui.ImageByteFormat.rawRgba) {
@@ -208,38 +205,17 @@ class CkImage implements ui.Image {
208205
final SkData skData = skImage.encodeToData(); //defaults to PNG 100%
209206
// make a copy that we can return
210207
bytes = Uint8List.fromList(canvasKit.getDataBytes(skData));
208+
skData.delete();
211209
}
212210

213211
final ByteData data = bytes.buffer.asByteData(0, bytes.length);
214212
return Future<ByteData>.value(data);
215213
}
216214

217215
@override
218-
String toString() => '[$width\u00D7$height]';
219-
}
220-
221-
/// A [Codec] that wraps an `SkAnimatedImage`.
222-
class CkAnimatedImageCodec implements ui.Codec {
223-
CkAnimatedImage animatedImage;
224-
225-
CkAnimatedImageCodec(this.animatedImage);
226-
227-
@override
228-
void dispose() {
229-
animatedImage.dispose();
230-
}
231-
232-
@override
233-
int get frameCount => animatedImage.frameCount;
234-
235-
@override
236-
int get repetitionCount => animatedImage.repetitionCount;
237-
238-
@override
239-
Future<ui.FrameInfo> getNextFrame() {
240-
final Duration duration = animatedImage.decodeNextFrame();
241-
final CkImage image = animatedImage.currentFrameAsImage;
242-
return Future<ui.FrameInfo>.value(AnimatedImageFrameInfo(duration, image));
216+
String toString() {
217+
assert(_debugCheckIsNotDisposed());
218+
return '[$width\u00D7$height]';
243219
}
244220
}
245221

0 commit comments

Comments
 (0)