Skip to content

Commit 89a46af

Browse files
authored
Implement Image.clone for CanvasKit (flutter#21555)
1 parent 2d42c16 commit 89a46af

File tree

5 files changed

+168
-20
lines changed

5 files changed

+168
-20
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,8 @@ class SkAnimatedImage {
683683
///
684684
/// This object is no longer usable after calling this method.
685685
external void delete();
686+
external bool isAliasOf(SkAnimatedImage other);
687+
external bool isDeleted();
686688
}
687689

688690
@JS()
@@ -698,6 +700,8 @@ class SkImage {
698700
);
699701
external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY);
700702
external SkData encodeToData();
703+
external bool isAliasOf(SkImage other);
704+
external bool isDeleted();
701705
}
702706

703707
@JS()

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

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,15 @@ class CkAnimatedImage implements ui.Image {
4343
// being garbage-collected, or by an explicit call to [delete].
4444
late final SkiaObjectBox box;
4545

46-
CkAnimatedImage(this._skAnimatedImage) {
47-
box = SkiaObjectBox(this, _skAnimatedImage as SkDeletable);
46+
CkAnimatedImage(SkAnimatedImage skAnimatedImage) : this._(skAnimatedImage, null);
47+
48+
CkAnimatedImage._(this._skAnimatedImage, SkiaObjectBox? boxToClone) {
49+
if (boxToClone != null) {
50+
assert(boxToClone.skObject == _skAnimatedImage);
51+
box = boxToClone.clone(this);
52+
} else {
53+
box = SkiaObjectBox(this, _skAnimatedImage as SkDeletable);
54+
}
4855
}
4956

5057
@override
@@ -53,13 +60,17 @@ class CkAnimatedImage implements ui.Image {
5360
}
5461

5562
@override
56-
ui.Image clone() => this;
63+
ui.Image clone() => CkAnimatedImage._(_skAnimatedImage, box);
5764

5865
@override
59-
bool isCloneOf(ui.Image other) => other == this;
66+
bool isCloneOf(ui.Image other) {
67+
return other is CkAnimatedImage
68+
&& other._skAnimatedImage.isAliasOf(_skAnimatedImage);
69+
}
6070

6171
@override
62-
List<StackTrace>? debugGetOpenHandleStackTraces() => null;
72+
List<StackTrace>? debugGetOpenHandleStackTraces() => box.debugGetStackTraces();
73+
6374
int get frameCount => _skAnimatedImage.getFrameCount();
6475

6576
/// Decodes the next frame and returns the frame duration.
@@ -116,8 +127,15 @@ class CkImage implements ui.Image {
116127
// being garbage-collected, or by an explicit call to [delete].
117128
late final SkiaObjectBox box;
118129

119-
CkImage(this.skImage) {
120-
box = SkiaObjectBox(this, skImage as SkDeletable);
130+
CkImage(SkImage skImage) : this._(skImage, null);
131+
132+
CkImage._(this.skImage, SkiaObjectBox? boxToClone) {
133+
if (boxToClone != null) {
134+
assert(boxToClone.skObject == skImage);
135+
box = boxToClone.clone(this);
136+
} else {
137+
box = SkiaObjectBox(this, skImage as SkDeletable);
138+
}
121139
}
122140

123141
@override
@@ -126,13 +144,16 @@ class CkImage implements ui.Image {
126144
}
127145

128146
@override
129-
ui.Image clone() => this;
147+
ui.Image clone() => CkImage._(skImage, box);
130148

131149
@override
132-
bool isCloneOf(ui.Image other) => other == this;
150+
bool isCloneOf(ui.Image other) {
151+
return other is CkImage
152+
&& other.skImage.isAliasOf(skImage);
153+
}
133154

134155
@override
135-
List<StackTrace>? debugGetOpenHandleStackTraces() => null;
156+
List<StackTrace>? debugGetOpenHandleStackTraces() => box.debugGetStackTraces();
136157

137158
@override
138159
int get width => skImage.width();

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

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -239,23 +239,46 @@ abstract class OneShotSkiaObject<T extends Object> extends SkiaObject<T> {
239239
}
240240
}
241241

242-
/// Manages the lifecycle of a Skia object owned by a wrapper object.
242+
/// Uses reference counting to manage the lifecycle of a Skia object owned by a
243+
/// wrapper object.
243244
///
244-
/// When the wrapper is garbage collected, deletes the corresponding
245-
/// [skObject] (only in browsers that support weak references).
245+
/// When the wrapper is garbage collected, decrements the refcount (only in
246+
/// browsers that support weak references).
246247
///
247-
/// The [delete] method can be used to eagerly delete the [skObject]
248-
/// before the wrapper is garbage collected.
248+
/// The [delete] method can be used to eagerly decrement the refcount before the
249+
/// wrapper is garbage collected.
249250
///
250251
/// The [delete] method may be called any number of times. The box
251252
/// will only delete the object once.
252253
class SkiaObjectBox {
253-
SkiaObjectBox(Object wrapper, this.skObject) {
254+
SkiaObjectBox(Object wrapper, SkDeletable skObject)
255+
: this._(wrapper, skObject, <SkiaObjectBox>{});
256+
257+
SkiaObjectBox._(Object wrapper, this.skObject, this._refs) {
258+
if (assertionsEnabled) {
259+
_debugStackTrace = StackTrace.current;
260+
}
261+
_refs.add(this);
254262
if (browserSupportsFinalizationRegistry) {
255263
boxRegistry.register(wrapper, this);
256264
}
257265
}
258266

267+
/// Reference handles to the same underlying [skObject].
268+
final Set<SkiaObjectBox> _refs;
269+
270+
late final StackTrace? _debugStackTrace;
271+
/// If asserts are enabled, the [StackTrace]s representing when a reference
272+
/// was created.
273+
List<StackTrace>? debugGetStackTraces() {
274+
if (assertionsEnabled) {
275+
return _refs
276+
.map<StackTrace>((SkiaObjectBox box) => box._debugStackTrace!)
277+
.toList();
278+
}
279+
return null;
280+
}
281+
259282
/// The Skia object whose lifecycle is being managed.
260283
final SkDeletable skObject;
261284

@@ -269,16 +292,33 @@ class SkiaObjectBox {
269292
box.delete();
270293
}));
271294

272-
/// Deletes the [skObject].
295+
/// Returns a clone of this object, which increases its reference count.
296+
///
297+
/// Clones must be [dispose]d when finished.
298+
SkiaObjectBox clone(Object wrapper) {
299+
assert(!_isDeleted, 'Cannot clone from a deleted handle.');
300+
assert(_refs.isNotEmpty);
301+
return SkiaObjectBox._(wrapper, skObject, _refs);
302+
}
303+
304+
/// Decrements the reference count for the [skObject].
273305
///
274306
/// Does nothing if the object has already been deleted.
307+
///
308+
/// If this causes the reference count to drop to zero, deletes the
309+
/// [skObject].
275310
void delete() {
276311
if (_isDeleted) {
312+
assert(!_refs.contains(this));
277313
return;
278314
}
315+
final bool removed = _refs.remove(this);
316+
assert(removed);
279317
_isDeleted = true;
280-
_skObjectDeleteQueue.add(skObject);
281-
_skObjectCollector ??= _scheduleSkObjectCollection();
318+
if (_refs.isEmpty) {
319+
_skObjectDeleteQueue.add(skObject);
320+
_skObjectCollector ??= _scheduleSkObjectCollection();
321+
}
282322
}
283323
}
284324

lib/web_ui/test/canvaskit/image_test.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,27 @@ void testMain() {
3838
expect(image.box.isDeleted, true);
3939
});
4040

41+
test('CkAnimatedImage can be cloned and explicitly disposed of', () async {
42+
final SkAnimatedImage skAnimatedImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage);
43+
final CkAnimatedImage image = CkAnimatedImage(skAnimatedImage);
44+
final CkAnimatedImage imageClone = image.clone();
45+
46+
expect(image.isCloneOf(imageClone), true);
47+
expect(image.box.isDeleted, false);
48+
await Future<void>.delayed(Duration.zero);
49+
expect(skAnimatedImage.isDeleted(), false);
50+
image.dispose();
51+
expect(image.box.isDeleted, true);
52+
expect(imageClone.box.isDeleted, false);
53+
await Future<void>.delayed(Duration.zero);
54+
expect(skAnimatedImage.isDeleted(), false);
55+
imageClone.dispose();
56+
expect(image.box.isDeleted, true);
57+
expect(imageClone.box.isDeleted, true);
58+
await Future<void>.delayed(Duration.zero);
59+
expect(skAnimatedImage.isDeleted(), true);
60+
});
61+
4162
test('CkImage toString', () {
4263
final SkImage skImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage).getCurrentFrame();
4364
final CkImage image = CkImage(skImage);
@@ -54,6 +75,27 @@ void testMain() {
5475
image.dispose();
5576
expect(image.box.isDeleted, true);
5677
});
78+
79+
test('CkImage can be explicitly disposed of when cloned', () async {
80+
final SkImage skImage = canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage).getCurrentFrame();
81+
final CkImage image = CkImage(skImage);
82+
final CkImage imageClone = image.clone();
83+
84+
expect(image.isCloneOf(imageClone), true);
85+
expect(image.box.isDeleted, false);
86+
await Future<void>.delayed(Duration.zero);
87+
expect(skImage.isDeleted(), false);
88+
image.dispose();
89+
expect(image.box.isDeleted, true);
90+
expect(imageClone.box.isDeleted, false);
91+
await Future<void>.delayed(Duration.zero);
92+
expect(skImage.isDeleted(), false);
93+
imageClone.dispose();
94+
expect(image.box.isDeleted, true);
95+
expect(imageClone.box.isDeleted, true);
96+
await Future<void>.delayed(Duration.zero);
97+
expect(skImage.isDeleted(), true);
98+
});
5799
// TODO: https://github.com/flutter/flutter/issues/60040
58100
}, skip: isIosSafari);
59101
}

lib/web_ui/test/canvaskit/skia_objects_cache_test.dart

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:ui/ui.dart' as ui;
1212
import 'package:ui/src/engine.dart';
1313

1414
import 'common.dart';
15+
import '../matchers.dart';
1516

1617
void main() {
1718
internalBootstrapBrowserTest(() => testMain);
@@ -153,9 +154,49 @@ void _tests() {
153154
expect(SkiaObjects.oneShotCache.debugContains(object2), isFalse);
154155
});
155156
});
157+
158+
159+
group(SkiaObjectBox, () {
160+
test('Records stack traces and respects refcounts', () async {
161+
TestOneShotSkiaObject.deleteCount = 0;
162+
final TestOneShotSkiaObject skObject = TestOneShotSkiaObject();
163+
final Object wrapper = Object();
164+
final SkiaObjectBox box = SkiaObjectBox(wrapper, skObject);
165+
166+
expect(box.debugGetStackTraces().length, 1);
167+
168+
final SkiaObjectBox clone = box.clone(wrapper);
169+
expect(clone, isNot(same(box)));
170+
expect(clone.debugGetStackTraces().length, 2);
171+
expect(box.debugGetStackTraces().length, 2);
172+
173+
box.delete();
174+
175+
expect(() => box.clone(wrapper), throwsAssertionError);
176+
177+
expect(box.isDeleted, true);
178+
179+
// Let any timers elapse.
180+
await Future<void>.delayed(Duration.zero);
181+
expect(TestOneShotSkiaObject.deleteCount, 0);
182+
183+
expect(clone.debugGetStackTraces().length, 1);
184+
expect(box.debugGetStackTraces().length, 1);
185+
186+
clone.delete();
187+
expect(() => clone.clone(wrapper), throwsAssertionError);
188+
189+
// Let any timers elapse.
190+
await Future<void>.delayed(Duration.zero);
191+
expect(TestOneShotSkiaObject.deleteCount, 1);
192+
193+
expect(clone.debugGetStackTraces().length, 0);
194+
expect(box.debugGetStackTraces().length, 0);
195+
});
196+
});
156197
}
157198

158-
class TestOneShotSkiaObject extends OneShotSkiaObject<SkPaint> {
199+
class TestOneShotSkiaObject extends OneShotSkiaObject<SkPaint> implements SkDeletable {
159200
static int deleteCount = 0;
160201

161202
TestOneShotSkiaObject() : super(SkPaint());

0 commit comments

Comments
 (0)