Skip to content

Image disposes its image too early, causing errors such as "Cannot clone a disposed image" #110129

@fzyzcjy

Description

@fzyzcjy

Bug analysis and solution

Summary: Looking at Image. When its new image is decoded, it will immediately dispose its old image handle. However, this is logically wrong - the old image handle may still be used in the (near) future.

At the first glance, this looks impossible, because when the old image handle is disposed, we will no longer give it to child widget tree. And we all know that, Flutter builds widget tree from ancestors to descendants. Therefore, as long as the ancestor (Image) provides the new image handle to its children (RawImage), its children should not use the old image handle after it is disposed.

However, it is not true and this bug is indeed there. There are some special but very widely used widgets that "breaks" this mental modal, such as the LayoutBuilder. When we have something like WidgetOne(child: LayoutBuilder(builder: WidgetTwo())), and WidgetOne and WidgetTwo both markNeedsBuild (such as by setState), then the build order is indeed: WidgetOne - WidgetTwo(with old data) - LayoutBuilder - WidgetTwo(with new data). The "WidgetTwo(with old data)" will cause bugs in our case. In our case, the "old data" is equivalent to "the image handle that we have disposed", so we are indeed using a disposed image handle.

Reproducible samples are given below to verify the theoretical analysis.


Steps to Reproduce

Run the following self-contained test. (I added a lot of logs so you can see what is happening)

Expected results:
No error

Actual results:
Errors. Please have a look at logs below.

Code sample
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  const imageData =
      'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=';

  testWidgets('hi', (tester) async {
    final outerListenable = ValueNotifier<int>(0);
    final innerListenable = ValueNotifier<int>(0);

    var frameBuilderCountWithImage = 0;
    var image = MemoryImage(base64Decode(imageData));

    Future<void> runAsyncAndIdle() async {
      for (var i = 0; i < 20; ++i) {
        await tester.runAsync(() => Future<void>.delayed(Duration.zero));
        await tester.idle();
      }
    }

    await tester.pumpWidget(MaterialApp(
      home: Center(
        child: ValueListenableBuilder<void>(
          valueListenable: outerListenable,
          builder: (_, __, ___) {
            debugPrint('build outer-Observer');
            return Image(
              image: image,
              frameBuilder: (_, child, __, ___) {
                final peekChildImage = ((child as Semantics).child! as RawImage).image;
                debugPrint('build Image.frameBuilder peekChildImage=$peekChildImage');

                if (peekChildImage != null) {
                  frameBuilderCountWithImage++;
                }

                return LayoutBuilder(builder: (_, __) {
                  debugPrint('build LayoutBuilder peekChildImage=$peekChildImage');

                  return ValueListenableBuilder(
                    valueListenable: innerListenable,
                    builder: (_, __, ___) {
                      debugPrint('build inner-Observer peekChildImage=$peekChildImage');

                      return KeyedSubtree(
                        key: UniqueKey(),
                        child: child,
                      );
                    },
                  );
                });
              },
            );
          },
        ),
      ),
    ));

    while (frameBuilderCountWithImage == 0) {
      debugPrint('pump to wait for frameBuilderCountWithImage');
      await runAsyncAndIdle();
      await tester.pump();
    }

    debugPrint('--- modify observable ---');
    image = MemoryImage(base64Decode(imageData)); // imagine it is a new image
    outerListenable.value++;
    innerListenable.value++;

    debugPrint('--- pump ---');
    frameBuilderCountWithImage = 0;
    while (frameBuilderCountWithImage == 0) {
      debugPrint('pump to wait for frameBuilderCountWithImage');
      await runAsyncAndIdle();
      innerListenable.value++;
      await tester.pump();
    }
  });
}
Logs
/Users/tom/fvm/default/bin/flutter --no-color test --machine --start-paused test/hello.dart
Testing started at 08:53 ...

build outer-Observer
build Image.frameBuilder peekChildImage=null
build LayoutBuilder peekChildImage=null
build inner-Observer peekChildImage=null
hi RawImage#70162 createRenderObject image=Null#007db
pump to wait for frameBuilderCountWithImage
build Image.frameBuilder peekChildImage=[1×1]
build LayoutBuilder peekChildImage=[1×1]
build inner-Observer peekChildImage=[1×1]
hi RawImage#1016a createRenderObject image=Image#da4a8
--- modify observable ---
--- pump ---
pump to wait for frameBuilderCountWithImage
build outer-Observer
build Image.frameBuilder peekChildImage=null
build inner-Observer peekChildImage=[1×1]
hi RawImage#1016a createRenderObject image=Image#da4a8
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following StateError was thrown building KeyedSubtree-[#358f6]:
Bad state: Cannot clone a disposed image.
The clone() method of a previously-disposed Image was called. Once an Image object has been
disposed, it can no longer be used to create handles, as the underlying data may have been released.

The relevant error-causing widget was:
  KeyedSubtree-[#358f6]
  KeyedSubtree:file:///Users/tom/Main/yplusplus/frontend/yplusplus_core/test/hello.dart:48:30

When the exception was thrown, this was the stack:
#0      Image.clone (dart:ui/painting.dart:1815:7)
#1      RawImage.createRenderObject (package:flutter/src/widgets/basic.dart:5794:21)
#2      RenderObjectElement.mount (package:flutter/src/widgets/framework.dart:5662:52)
...     Normal element mounting (10 frames)
#12     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3817:16)
#13     Element.updateChild (package:flutter/src/widgets/framework.dart:3545:20)
#14     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4832:16)
#15     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4977:11)
#16     Element.rebuild (package:flutter/src/widgets/framework.dart:4529:5)
#17     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2659:19)
#18     AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1183:19)
#19     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:370:5)
#20     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1146:15)
#21     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1083:9)
#22     AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:1050:9)
#25     TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#26     AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:1037:27)
#27     WidgetTester.pump.<anonymous closure> (package:flutter_test/src/widget_tester.dart:608:53)
#30     TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#31     WidgetTester.pump (package:flutter_test/src/widget_tester.dart:608:27)
#32     main.<anonymous closure> (file:///Users/tom/Main/yplusplus/frontend/yplusplus_core/test/hello.dart:79:20)
<asynchronous suspension>
<asynchronous suspension>
(elided 5 frames from dart:async and package:stack_trace)

════════════════════════════════════════════════════════════════════════════════════════════════════
build LayoutBuilder peekChildImage=null
build inner-Observer peekChildImage=null
hi RawImage#6170d createRenderObject image=Null#007db
pump to wait for frameBuilderCountWithImage
build Image.frameBuilder peekChildImage=[1×1]
build inner-Observer peekChildImage=null
hi RawImage#6170d createRenderObject image=Null#007db
build LayoutBuilder peekChildImage=[1×1]
build inner-Observer peekChildImage=[1×1]
hi RawImage#e64b1 createRenderObject image=Image#e025c
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following StateError was thrown building KeyedSubtree-[#358f6]:
Bad state: Cannot clone a disposed image.
The clone() method of a previously-disposed Image was called. Once an Image object has been
disposed, it can no longer be used to create handles, as the underlying data may have been released.

When the exception was thrown, this was the stack:
#0      Image.clone (dart:ui/painting.dart:1815:7)
#1      RawImage.createRenderObject (package:flutter/src/widgets/basic.dart:5794:21)
#2      RenderObjectElement.mount (package:flutter/src/widgets/framework.dart:5662:52)
...     Normal element mounting (10 frames)
#12     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3817:16)
#13     Element.updateChild (package:flutter/src/widgets/framework.dart:3545:20)
#14     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4832:16)
#15     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4977:11)
#16     Element.rebuild (package:flutter/src/widgets/framework.dart:4529:5)
#17     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2659:19)
#18     AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1183:19)
#19     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:370:5)
#20     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1146:15)
#21     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1083:9)
#22     AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:1050:9)
#25     TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#26     AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:1037:27)
#27     WidgetTester.pump.<anonymous closure> (package:flutter_test/src/widget_tester.dart:608:53)
#30     TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#31     WidgetTester.pump (package:flutter_test/src/widget_tester.dart:608:27)
#32     main.<anonymous closure> (file:///Users/tom/Main/yplusplus/frontend/yplusplus_core/test/hello.dart:79:20)
<asynchronous suspension>
<asynchronous suspension>
(elided 5 frames from dart:async and package:stack_trace)

════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY WIDGET INSPECTOR ╞══════════════════════════════════════════════════════════
The following assertion was thrown:
Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by
calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

When the exception was thrown, this was the stack:
#0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> (package:flutter/src/widgets/framework.dart:4186:9)
#1      Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4200:6)
#2      Element.visitAncestorElements (package:flutter/src/widgets/framework.dart:4299:12)
#3      _describeRelevantUserCode (package:flutter/src/widgets/widget_inspector.dart:3051:13)
#4      _parseDiagnosticsNode (package:flutter/src/widgets/widget_inspector.dart:2974:12)
#5      debugTransformDebugCreator (package:flutter/src/widgets/widget_inspector.dart:2950:21)
#6      _FlutterErrorDetailsNode.builder (package:flutter/src/foundation/assertions.dart:1291:31)
#7      DiagnosticableNode.getProperties (package:flutter/src/foundation/diagnostics.dart:3009:105)
#8      TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1244:63)
#9      TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1121:14)
#10     FlutterError.dumpErrorToConsole (package:flutter/src/foundation/assertions.dart:1014:13)
#11     _defaultTestExceptionReporter (package:flutter_test/src/test_exception_reporter.dart:32:16)
#12     TestWidgetsFlutterBinding._createTestCompletionHandler.<anonymous closure> (package:flutter_test/src/binding.dart:663:28)
#22     TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart)
<asynchronous suspension>
(elided 9 frames from dart:async, dart:async-patch, and package:stack_trace)

This exception was caught while trying to describe the user-relevant code of another error.
════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY WIDGET INSPECTOR ╞══════════════════════════════════════════════════════════
The following assertion was thrown:
Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by
calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

When the exception was thrown, this was the stack:
#0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> (package:flutter/src/widgets/framework.dart:4186:9)
#1      Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4200:6)
#2      Element.visitAncestorElements (package:flutter/src/widgets/framework.dart:4299:12)
#3      _describeRelevantUserCode (package:flutter/src/widgets/widget_inspector.dart:3051:13)
#4      _parseDiagnosticsNode (package:flutter/src/widgets/widget_inspector.dart:2974:12)
#5      debugTransformDebugCreator (package:flutter/src/widgets/widget_inspector.dart:2950:21)
#6      _FlutterErrorDetailsNode.builder (package:flutter/src/foundation/assertions.dart:1291:31)
#7      DiagnosticableNode.getProperties (package:flutter/src/foundation/diagnostics.dart:3009:105)
#8      TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1244:63)
#9      TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1121:14)
#10     FlutterError.dumpErrorToConsole (package:flutter/src/foundation/assertions.dart:1014:13)
#11     _defaultTestExceptionReporter (package:flutter_test/src/test_exception_reporter.dart:32:16)
#12     TestWidgetsFlutterBinding._createTestCompletionHandler.<anonymous closure> (package:flutter_test/src/binding.dart:663:28)
#22     TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart)
<asynchronous suspension>
(elided 9 frames from dart:async, dart:async-patch, and package:stack_trace)

This exception was caught while trying to describe the user-relevant code of another error.
════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY WIDGET INSPECTOR ╞══════════════════════════════════════════════════════════
The following assertion was thrown:
Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by
calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

When the exception was thrown, this was the stack:
#0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> (package:flutter/src/widgets/framework.dart:4186:9)
#1      Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4200:6)
#2      Element.visitAncestorElements (package:flutter/src/widgets/framework.dart:4299:12)
#3      _describeRelevantUserCode (package:flutter/src/widgets/widget_inspector.dart:3051:13)
#4      _parseDiagnosticsNode (package:flutter/src/widgets/widget_inspector.dart:2974:12)
#5      debugTransformDebugCreator (package:flutter/src/widgets/widget_inspector.dart:2950:21)
#6      _FlutterErrorDetailsNode.builder (package:flutter/src/foundation/assertions.dart:1291:31)
#7      DiagnosticableNode.emptyBodyDescription (package:flutter/src/foundation/diagnostics.dart:3006:77)
#8      TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1280:14)
#9      TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1121:14)
#10     FlutterError.dumpErrorToConsole (package:flutter/src/foundation/assertions.dart:1014:13)
#11     _defaultTestExceptionReporter (package:flutter_test/src/test_exception_reporter.dart:32:16)
#12     TestWidgetsFlutterBinding._createTestCompletionHandler.<anonymous closure> (package:flutter_test/src/binding.dart:663:28)
#22     TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart)
<asynchronous suspension>
(elided 9 frames from dart:async, dart:async-patch, and package:stack_trace)

This exception was caught while trying to describe the user-relevant code of another error.
════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY WIDGET INSPECTOR ╞══════════════════════════════════════════════════════════
The following assertion was thrown:
Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by
calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

When the exception was thrown, this was the stack:
#0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> (package:flutter/src/widgets/framework.dart:4186:9)
#1      Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4200:6)
#2      Element.visitAncestorElements (package:flutter/src/widgets/framework.dart:4299:12)
#3      _describeRelevantUserCode (package:flutter/src/widgets/widget_inspector.dart:3051:13)
#4      _parseDiagnosticsNode (package:flutter/src/widgets/widget_inspector.dart:2974:12)
#5      debugTransformDebugCreator (package:flutter/src/widgets/widget_inspector.dart:2950:21)
#6      _FlutterErrorDetailsNode.builder (package:flutter/src/foundation/assertions.dart:1291:31)
#7      DiagnosticableNode.emptyBodyDescription (package:flutter/src/foundation/diagnostics.dart:3006:77)
#8      TextTreeRenderer._debugRender (package:flutter/src/foundation/diagnostics.dart:1280:14)
#9      TextTreeRenderer.render (package:flutter/src/foundation/diagnostics.dart:1121:14)
#10     FlutterError.dumpErrorToConsole (package:flutter/src/foundation/assertions.dart:1014:13)
#11     _defaultTestExceptionReporter (package:flutter_test/src/test_exception_reporter.dart:32:16)
#12     TestWidgetsFlutterBinding._createTestCompletionHandler.<anonymous closure> (package:flutter_test/src/binding.dart:663:28)
#22     TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart)
<asynchronous suspension>
(elided 9 frames from dart:async, dart:async-patch, and package:stack_trace)

This exception was caught while trying to describe the user-relevant code of another error.
════════════════════════════════════════════════════════════════════════════════════════════════════

Test failed. See exception logs above.
The test description was: hi
no error
latest

A shorter reproducible sample, if you are interested:

Details
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('hi', (tester) async {
    const imageData =
        'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=';

    final outerListenable = ValueNotifier<int>(0);
    final innerListenable = ValueNotifier<int>(0);

    var imageLoaded = false;
    var image = MemoryImage(base64Decode(imageData));

    Future<void> runAsyncAndIdle() async {
      for (var i = 0; i < 20; ++i) {
        await tester.runAsync(() => Future<void>.delayed(Duration.zero));
        await tester.idle();
      }
    }

    await tester.pumpWidget(ValueListenableBuilder(
      valueListenable: outerListenable,
      builder: (_, __, ___) => Image(
        image: image,
        frameBuilder: (_, child, __, ___) {
          if (((child as Semantics).child! as RawImage).image != null) imageLoaded = true;
          return LayoutBuilder(
            builder: (_, __) => ValueListenableBuilder(
              valueListenable: innerListenable,
              builder: (_, __, ___) => KeyedSubtree(
                key: UniqueKey(),
                child: child,
              ),
            ),
          );
        },
      ),
    ));

    while (!imageLoaded) {
      await runAsyncAndIdle();
      await tester.pump();
    }

    image = MemoryImage(base64Decode(imageData)); // imagine it is a new image
    outerListenable.value++;

    imageLoaded = false;
    while (!imageLoaded) {
      await runAsyncAndIdle();
      innerListenable.value++;
      await tester.pump();
    }
  });
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    a: imagesLoading, displaying, rendering imagesc: crashStack traces logged to the consolefound in release: 3.0Found to occur in 3.0found in release: 3.1Found to occur in 3.1frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer version

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions