Home > front end >  Flutter DevTool: Class instance not removed on screen dispose
Flutter DevTool: Class instance not removed on screen dispose

Time:12-06

My application (through multiple class instances, static classes and get_it dependency injector) contains some nasty memory leaks. I'm on a mission to find and resolve them.

But first, I wanted to establish the ground truth with a small example. With profiling of that example, I noticed that inside Flutter DevTools -> Memory -> Heap Snapshot my class instance is still present even though I navigated away from that screen (that initialised this class) and therefor I assumed that it will be removed with GC from the heap memory.

This is not a case, and the ProductService class is still present in the heap memory after I navigate back to the main screen.

ProductService not removed from heap memory

Here is my complete example app, if somebody would like to reproduce this issue.

main.dart:
import 'package:flutter/material.dart';
import 'package:memory_leak_example/second.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First Route'),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Open route'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => const SecondRoute()),
            );
          },
        ),
      ),
    );
  }
}
second.dart:
import 'package:flutter/material.dart';
import 'package:memory_leak_example/product_service.dart';

class SecondRoute extends StatefulWidget {
  const SecondRoute({super.key});

  @override
  State<SecondRoute> createState() => _SecondRouteState();
}

class _SecondRouteState extends State<SecondRoute> {
  late final ProductService productService = ProductService();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Route'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: StreamBuilder<Object>(
            stream: productService.timer$,
            builder: (context, snapshot) {
              return Text('Go back (${snapshot.data})!');
            },
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    productService.onDispose();
    super.dispose();
  }
}
product_service.dart:
import 'dart:async';

import 'bridge.dart';

class ProductService {
  ProductService() {
    sub = timer$.listen((int tick) {
      print(tick);
    });
  }

  final Stream<int> timer$ = Bridge.timer$;

  late final StreamSubscription<int> sub;

  void onDispose() {
    sub.cancel();
  }
}
bridge.dart
import 'dart:async';

class Bridge {
  Bridge._();

  static Stream<int> timer$ = Stream<int>.periodic(const Duration(seconds: 1), (x) => x).asBroadcastStream();
}

Why is ProductService instance not removed from the heap memory after SecondScreen is disposed?

CodePudding user response:

TL;DR: heap snapshots can contain unreachable instances and ProductService will be GC'd

Heap snapshots from the Dart VM can contain both reachable and unreachable objects in the heap, so it's possible to see instances in the snapshot that have no references but have not yet been collected by the GC.

In most cases, the GC is triggered as a result of additional allocations from the heap (or in the case of Flutter, when the framework thinks there's some time to GC without interrupting a frame), so it's possible that an object that isn't reachable won't collected for the duration of the program if it doesn't perform enough allocations. You can force a GC through the DevTools UI and then request a heap snapshot to have more confidence that you're not seeing an unreachable object.

With your example code, I was able to confirm that this instance of ProductService is simply unreachable and will be collected during a future GC. I opened the inspector for ProductService in Observatory and was able to confirm there was a single instance of ProductService when the second route was visible:

enter image description here

I then returned to the first route and refreshed in Observatory to confirm the instance was no longer live:

enter image description here

This sort of analysis isn't currently possible in the stable version of DevTools, but myself and other DevTools team members have been working on overhauling the memory tooling to make it easier to identify these sorts of scenarios. Some of the current tooling doesn't help developers debug issues the way we'd hoped, so keep an eye out on this tab in the next Dart/Flutter releases for significantly improved tooling.

  • Related