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.
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:
I then returned to the first route and refreshed in Observatory to confirm the instance was no longer live:
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.