Skip to content

[android][engine] Native memory grows continuously with CustomPainter + ui.Paragraph under high-frequency repaint (Flutter 3.32) #189756

Open

Description

@fangxinbaize

Steps to reproduce

Steps to Reproduce

  1. Run the attached minimal app on an Android device (tested on ARM64).
  2. Open Android Studio Profiler or run adb shell dumpsys meminfo <package>.
  3. Observe Native Heap / Graphics memory while the app is running.

Expected results

Expected behavior
Native memory should stabilize. Calling ui.Paragraph.dispose() should release underlying Skia/HWUI resources in a timely manner.

Actual results

Actual behavior
Native memory grows monotonically and never fully reclaims, even though Dart heap remains stable. After several minutes, Graphics and Unknown categories dominate memory usage.

Code sample

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

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter StringTOASCII',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'StringTOASCII'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _isAutoGenerating = false;
  Timer? _timer;
  final Random _random = Random();

  /// ✅ 用 ValueNotifier 驱动 CustomPainter
  final ValueNotifier<String> _asciiNotifier =
  ValueNotifier('等待生成...');

  String _generateRandomString(int length) {
    const chars =
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#\$%^&*()_+-=[]{}|;:,.<>?';
    return String.fromCharCodes(
      Iterable.generate(
        length,
            (_) => chars.codeUnitAt(_random.nextInt(chars.length)),
      ),
    );
  }

  String _convertToAscii(String input) {
    return input.runes
        .map((code) =>
        code.toRadixString(16).padLeft(2, '0').toUpperCase())
        .join(' ');
  }

  void _startAutoGenerate() {
    if (_isAutoGenerating) return;
    setState(() => _isAutoGenerating = true);

    _timer = Timer.periodic(const Duration(milliseconds: 500), (_) {
      final randomLength = _random.nextInt(30) + 1;
      final randomString = _generateRandomString(randomLength);
      final ascii = _convertToAscii(randomString);

      /// ✅ 只更新数据,不 rebuild widget 树
      _asciiNotifier.value = ascii;
    });
  }

  void _stopAutoGenerate() {
    if (!_isAutoGenerating) return;
    _timer?.cancel();
    setState(() => _isAutoGenerating = false);
  }

  @override
  void dispose() {
    _timer?.cancel();
    _asciiNotifier.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            /// ✅ 固定高度,避免 relayout
            Container(
              width: double.infinity,
              height: 120,
              margin: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.grey),
                borderRadius: BorderRadius.circular(8),
                color: Colors.grey.withOpacity(0.05),
              ),
              child: CustomPaint(
                painter: HexPainter(_asciiNotifier),
                size: const Size(double.infinity, 120),
              ),
            ),

            ElevatedButton(
              onPressed: _isAutoGenerating ? null : _startAutoGenerate,
              child: const Text('开始自动生成'),
            ),
            ElevatedButton(
              onPressed: _isAutoGenerating ? _stopAutoGenerate : null,
              child: const Text('停止自动生成'),
            ),
          ],
        ),
      ),
    );
  }
}

/// ✅ 用 Listenable 驱动,保证每次 value 变化都会 repaint
class HexPainter extends CustomPainter {
  final ValueNotifier<String> notifier;
  ui.Paragraph? _paragraph;

  HexPainter(this.notifier) : super(repaint: notifier);

  void _updateParagraph(String text, double maxWidth) {
    final builder = ui.ParagraphBuilder(
      ui.ParagraphStyle(
        textAlign: TextAlign.left,
        fontSize: 16,
        fontFamily: 'monospace',
        fontWeight: ui.FontWeight.bold,
      ),
    )
      ..pushStyle(ui.TextStyle(color: Colors.black))
      ..addText('HEX编码:\n$text');

    _paragraph?.dispose(); // ✅ 先释放旧的
    _paragraph = builder.build()
      ..layout(ui.ParagraphConstraints(width: maxWidth));
  }

  @override
  void paint(Canvas canvas, Size size) {
    final text = notifier.value;
    _updateParagraph(text, size.width - 32);

    canvas.drawParagraph(
      _paragraph!,
      const Offset(16, 0),
    );
  }

  @override
  bool shouldRepaint(covariant HexPainter oldDelegate) => false;

}

This is a minimal reproducible example. No WebView, no PlatformView, no images.

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Details
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.32.0, on Microsoft Windows [版本 10.0.26200.8875], locale zh-CN)
[√] Windows Version (Windows 11 or higher, 25H2, 2009)
[!] Android toolchain - develop for Android devices (Android SDK version 36.1.0)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[√] Chrome - develop for the web
[X] Visual Studio - develop Windows apps
    X Visual Studio not installed; this is necessary to develop Windows apps.
      Download at https://visualstudio.microsoft.com/downloads/.
      Please install the "Desktop development with C++" workload, including all of its default components
[√] Android Studio (version 2024.3)
[√] VS Code (version unknown)
    X Unable to determine VS Code version.
[√] Connected device (4 available)

Activity

  1. fangxinbaize commented on Jul 21, 2026

    @fangxinbaize
    Author

    Note: the ! (Android licenses) and [X] Visual Studio in doctor are unrelated to this issue (Windows host only, target is Android arm64). Licenses have since been accepted; behavior unchanged on a clean flutter doctor run.

    This minimal repro was extracted from a real app:

    • Android, WebView (Hybrid Composition) rendering a 3D model
    • pose + timestamp updated every ~120ms via evaluateJavascript
    • native heap (dumpsys meminfo) grows monotonically, Graphics/Unknown dominate
    • Dart heap flat

    The sample above removes WebView/PlatformView entirely to isolate:
    high-frequency CustomPainter.repaint → ui.Paragraph → native Skia/HWUI allocation
    which appears to be one contributor to the real app's leak.

  2. saurabh-mirajkar commented on Jul 21, 2026

    @saurabh-mirajkar
    Contributor

    Thanks for the detailed report. I tested it on a physical Pixel 6 (Android 17) but wasn't able to reproduce the reported behavior.

    Over several minutes of running (~30 mins), I observed only a small increase in Native Heap before it appeared to stabilize, while the Graphics memory remained essentially unchanged. I also noticed the report was filed using Flutter 3.32.0, whereas the current stable release is 3.44.7.

    Could you confirm whether this is still reproducible on Flutter 3.44.7? If so, could you also share a memory profile (Android Studio Profiler or adb shell dumpsys meminfo) showing the continuous growth over time?

  3. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    and removed
    in triagePresently being triaged by the triage team
    on Jul 21, 2026
  4. sky125801 commented on Jul 21, 2026

    @sky125801

    @saurabh-mirajkar @fangxinbaize
    I also encountered the issue of native heap continuously growing:
    Flutter 3.44, Android 16, Galaxy S25 Ultra.
    In addition, I ran the same Flutter app on Windows and observed the same native heap growth phenomenon.
    The following is a demo that can quickly reproduce the issue.

    main_bloc.dart

    import 'dart:async';
    import 'package:bloc/bloc.dart';
    import 'package:memory_demo/bloc/main_response.dart';
    import 'main_event.dart';
    import 'main_state.dart';
    
    class MainBloc extends Bloc<MainEvent, MainState> {
      Timer? _timer;
      final Duration _duration = Duration(milliseconds: 10);
      int _data = 0;
      MainBloc() : super(MainState(response: MainResponse(value: 0))) {
        on<MainEventStart>(_onStart);
        on<MainEventLoop>(_onLoop);
      }
    
    
      FutureOr<void> _onStart(MainEventStart event, Emitter<MainState> emit) async {
        _timer?.cancel();
        _timer = Timer.periodic(_duration, (timer){
          add(MainEventLoop());
        });
      }
    
      FutureOr<void> _onLoop(MainEventLoop event, Emitter<MainState> emit) {
        emit(MainState(response: MainResponse(value: _data++)));
      }
    }

    main_event.dart

    abstract class MainEvent{}
    
    class MainEventStart extends MainEvent{}
    
    class MainEventLoop extends MainEvent{}
    

    main_state.dart

    import 'package:equatable/equatable.dart';
    import 'package:memory_demo/bloc/main_response.dart';
    
    class MainState extends Equatable{
      MainResponse response;
      MainState({required this.response});
    
      @override
      List<Object?> get props => [response];
    }
    

    main.dart

    import 'package:flutter/material.dart';
    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'package:memory_demo/bloc/main_bloc.dart';
    import 'package:memory_demo/bloc/main_state.dart';
    
    import 'bloc/main_event.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
          ),
          home: BlocProvider(
            create: (context) => MainBloc(),
            child: BlocBuilder<MainBloc,MainState>(
              buildWhen: (previous, current) => true,
              builder: (context,state) {
                return MyHomePage(title: 'Flutter Demo Home Page:${state.response.value}');
              }
            ),
          ),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key, required this.title});
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      void _incrementCounter() {
          context.read<MainBloc>().add(MainEventStart());
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.inversePrimary,
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text('You have pushed the button this many times:'),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        );
      }
    }
    
    adb shell dumpsys meminfo  --package com.example.memory_demo | findstr Native
      Native Heap    29640    29608        0       77    30992    36060    29339     6720
             Native Heap:    29608                          30992
    
    
    //after 20 minutes ....
    
    adb shell dumpsys meminfo  --package com.example.memory_demo | findstr Native
      Native Heap    32080    32048        0       77    33432    38520    35496     3023
             Native Heap:    32048                          33432
    
    
    //after 10 minutes ....
    
    adb shell dumpsys meminfo  --package com.example.memory_demo | findstr Native
      Native Heap    36185    36156        0       71    37536    42888    39702     3185
             Native Heap:    36156                          37536
    

    This phenomenon of native heap continuously growing will persist until memory is exhausted.

  5. fangxinbaize commented on Jul 22, 2026

    @fangxinbaize
    Author

    @saurabh-mirajkar
    Thank you for testing this issue on the Pixel 6!

    I have upgraded my environment to the version you mentioned (3.44, currently the latest stable) and retested on my device (Android 14).

    I can confirm that the issue is still reproducible on this version. Notably, I observed a clear correlation between the update frequency and the rate of memory growth: the faster the CustomPainter repaint interval, the quicker the Native Heap climbs.

    In addition, a 30‑minute test duration may be a bit too short; I would suggest running longer continuous tests to more clearly observe the long‑term trend of memory growth.

    Image Image
  6. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jul 22, 2026
  7. saurabh-mirajkar commented on Jul 22, 2026

    @saurabh-mirajkar
    Contributor

    @fangxinbaize Thanks for checking on the latest stable.

    I also tested the attached sample on Flutter 3.44.7 using a Pixel 6 and monitored adb shell dumpsys meminfo over approximately two hours. In my testing, the native heap increased during the initial warm-up period (~29.5 MB → ~33.6 MB), then stabilized around 32–33 MB rather than continuing to grow.

    Since we're seeing different behavior, could you share a longer series of adb shell dumpsys meminfo snapshots (or Android Studio Native Memory profiler captures) from your device on 3.44.x? That would help determine whether the native heap continues to grow over time or eventually reaches a steady state.

    I've attached my meminfo.log for reference.

  8. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    needs repro infoAutomated crash report whose cause isn't yet known
    and removed
    in triagePresently being triaged by the triage team
    on Jul 22, 2026
  9. fangxinbaize commented on Jul 23, 2026

    @fangxinbaize
    Author

    @saurabh-mirajkar
    Thank you for your continued testing. Our observations are indeed different. From yesterday afternoon to now, my app has been running continuously overnight, and the native heap has grown from over 40 MB to over 200 MB, with no sign of decreasing.

    Did you click the start button during your test? There is a button labeled "开始自动生成" on the app, and the HEX display panel keeps updating. If possible, could you change the generation interval from 500 ms to 100 ms, or even faster, say 50 ms? In the code, this method:

    void _startAutoGenerate() {
        if (_isAutoGenerating) return;
        setState(() => _isAutoGenerating = true);
        _timer = Timer.periodic(const Duration(milliseconds: **500**), (_) {
          final randomLength = _random.nextInt(30) + 1;
          final randomString = _generateRandomString(randomLength);
          final ascii = _convertToAscii(randomString);
    
          /// ✅ 只更新数据,不 rebuild widget 树
          _asciiNotifier.value = ascii;
        });}
    

    Attached is the memory info from my Android device this [morning.]

    meninfo.txt

    Thanks again for your support.

  10. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Jul 23, 2026
  11. saurabh-mirajkar commented on Jul 23, 2026

    @saurabh-mirajkar
    Contributor

    @fangxinbaize Thanks for the additional details.

    I retested this on the latest stable (Flutter 3.44.7) using the provided sample on a physical Pixel 6. I tapped on 开始自动生成 button and collected adb shell dumpsys meminfo snapshots approximately every 10 minutes over a ~4-hour run.

    I also observed gradual native heap growth during the test, although the growth rate on my device was lower than the one reported in the original issue. Attaching my meminfo log in case it's helpful for comparison. Routing this for further investigation.

  12. added
    platform-androidAndroid applications specifically
    engineflutter/engine related. See also e: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    team-androidOwned by Android platform team
    and removed
    in triagePresently being triaged by the triage team
    needs repro infoAutomated crash report whose cause isn't yet known
    on Jul 23, 2026
  13. fangxinbaize commented on Jul 27, 2026

    @fangxinbaize
    Author

    @saurabh-mirajkar
    Thank you for retesting and for sharing the detailed data! Your results perfectly corroborate the issue we're seeing—the Native Heap continues to grow gradually even on the stable channel.
    Our team is currently conducting long-term stability tests for our app, and this memory growth is preventing it from running reliably over extended periods. Could you please let us know if there are any updates on the investigation? We look forward to your reply.

  14. added
    P2Important issues not at the top of the work list
    triaged-androidTriaged by Android platform team
    on Jul 28, 2026
  15. fangxinbaize commented on Aug 17, 2026

    @fangxinbaize
    Author

    @jesswrd @saurabh-mirajkar

    linux_meminfo.txt

    Image

    I’ve reproduced this issue on both Ubuntu 22.04 and Windows, not only on Android. This indicates that the memory leak is not platform‑specific but rather a general defect in the engine layer.

    Moreover, the problem only becomes clearly visible after prolonged stress testing (e.g., >48 hours). Short runs (e.g., 2 hours) may show a seemingly stable memory footprint, but over extended periods the native heap keeps growing linearly, which confirms a true leak rather than temporary fluctuations.

    Attached is my test log from Ubuntu 22.04, with memory samples taken every 5 minutes. The data clearly illustrates the cumulative upward trend over time, which I hope will be useful for the team’s investigation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listengineflutter/engine related. See also e: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-androidAndroid applications specificallyteam-androidOwned by Android platform teamtriaged-androidTriaged by Android platform team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions