Skip to content

Memory leak in offscreen layout pass #187147

@wanggaowan

Description

@wanggaowan

Steps to reproduce

The following is the off-screen rendering logic. As long as the Code sample code is executed, the Android Native memory will grow and will not be released afterwards.

import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class OffScreenRender {
  static OffScreenRender? _singleton;

  final PipelineOwner pipelineOwner = PipelineOwner();
  final FocusManager focusManager = FocusManager();
  late final BuildOwner buildOwner;
  bool isDirty = false;

  OffScreenRender._inner() {
    buildOwner = BuildOwner(
      focusManager: focusManager,
      onBuildScheduled: () {
        isDirty = true;
      },
    );
  }

  static OffScreenRender get _instance {
    return _singleton ??= OffScreenRender._inner();
  }

  factory OffScreenRender() => _instance;

  /// Render the given Widget to [ui.Image]
  /// 
  /// [width] and [height] indicate the width and height of the imaged image. 
  /// If the child does not specify a size, this width and height is also 
  /// the maximum width and height of the child.
  Future<ui.Image> render(
    Widget child,
    double width,
    double height, {
    int maxTryCount = 10,
    double? outputPixelRatio,
    int renderDelay = 100,
  }) async {
    var view = PlatformDispatcher.instance.views.first;
    var devicePixelRatio = view.devicePixelRatio;

    final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();
    final RenderView renderView = RenderView(
      view: view,
      configuration: ViewConfiguration(
        physicalConstraints:
            BoxConstraints.expand(width: width, height: height),
        logicalConstraints: BoxConstraints.expand(width: width, height: height),
        devicePixelRatio: devicePixelRatio,
      ),
      child: repaintBoundary,
    );

    pipelineOwner.rootNode = renderView;
    renderView.prepareInitialFrame();

    final RenderObjectToWidgetElement<RenderBox> rootElement =
        RenderObjectToWidgetAdapter<RenderBox>(
      container: repaintBoundary,
      child: Directionality(
        textDirection: TextDirection.ltr,
        child: MediaQuery.fromView(
          view: view,
          child: child,
        ),
      ),
    ).attachToRenderTree(buildOwner);

    ui.Image? image;
    int tryCount = 0;
    try {
      do {
        isDirty = false;
        if (image != null) {
          image.dispose();
        }
        image = await _rebuildChild(pipelineOwner, rootElement, buildOwner,
            repaintBoundary, outputPixelRatio ?? devicePixelRatio);
        await Future.delayed(Duration(milliseconds: renderDelay));
        tryCount++;
      } while (tryCount < maxTryCount && isDirty);
      return image;
    } finally {
      pipelineOwner.rootNode = null;
      focusManager.dispose();
    }
  }

  Future<ui.Image> _rebuildChild(
      PipelineOwner pipelineOwner,
      Element child,
      BuildOwner buildOwner,
      RenderRepaintBoundary repaintBoundary,
      double outputPixelRatio) async {
    buildOwner.buildScope(child);
    buildOwner.finalizeTree();
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    return await repaintBoundary.toImage(pixelRatio: outputPixelRatio);
  }
}

Expected results

I want the memory to be released correctly after rendering is finished and image.dispose is called

Actual results

Memory continues to grow and will not be released, eventually causing App to flash back.

Code sample

Code sample
// After calling this method, android Native memory will gradually grow and will not be released, 
// even if image.dispose has been called to release memory
void test() async {
    for (var i = 0; i < 100; ++i) {
      var image = await OffScreenRender().render(Container(
        width: 100,
        height: 100,
        color: Colors.white,
        alignment: Alignment.center,
        child: const Text('test offscreen render'),
      ),100,100);
      // Process image, then release
      image.dispose();
    }
  }

Screenshots or Video

Image
Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
[✓]  Flutter (Channel [user-branch], 3.29.3, on macOS 15.7.5 24G624 darwin-arm64, locale zh-Hans-CN)
[✓]  Android toolchain - develop for Android devices (Android SDK version 36.0.0)
[✓]  Xcode - develop for iOS and macOS (Xcode 26.0.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2025.2)
[✓] Android Studio (version 2024.3)
[✓] IntelliJ IDEA Ultimate Edition (version 2025.2.4)
[✓] IntelliJ IDEA Ultimate Edition (version 2026.1.2)
[✓] VS Code (version 1.118.1)
[✓] Network resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    platform-androidAndroid applications specificallywaiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions