You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter DevTool问题:页面销毁后类实例未被移除

内存泄漏排查:页面销毁后ProductService实例未被GC回收

我的应用存在严重内存泄漏,涉及多类实例、静态类及get_it依赖注入器。为验证问题,我制作了小型测试示例,通过Flutter DevTool的Memory面板Heap Snapshot分析发现:退出初始化ProductService类的页面后,该类实例仍存在于堆内存中,未被垃圾回收(GC)移除。即使返回主页面,ProductService类实例依旧留存于堆内存中。

复现代码

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();
}

问题原因及解决方案

核心原因

问题出在**StreamBuilder仍持有对ProductService.timer$的引用**,而timer$指向Bridge类的静态广播流。虽然你在dispose中取消了ProductService内部的StreamSubscription,但StreamBuilder在页面销毁时不会自动取消对该流的订阅,导致ProductService实例被StreamBuilder的订阅链间接引用,无法被GC回收。

另外,Bridge.timer$是静态流,生命周期与应用一致。只要流本身存在,未取消的订阅就会持续持有相关对象引用,造成内存泄漏。

解决方案

方案1:手动管理StreamBuilder的订阅

在页面State类中保存StreamBuilder对应的订阅对象,在dispose时主动取消:

class _SecondRouteState extends State<SecondRoute> {
  late final ProductService productService = ProductService();
  StreamSubscription? _streamSubscription;
  int _currentTick = 0;

  @override
  void initState() {
    super.initState();
    // 手动订阅流并更新UI状态
    _streamSubscription = productService.timer$.listen((tick) {
      setState(() => _currentTick = tick);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Second Route')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.pop(context),
          child: Text('Go back ($_currentTick)!'),
        ),
      ),
    );
  }

  @override
  void dispose() {
    productService.onDispose();
    _streamSubscription?.cancel(); // 取消StreamBuilder对应的订阅
    super.dispose();
  }
}

方案2:绑定流与ProductService生命周期

将静态流改为ProductService的实例流,让流的生命周期与服务实例绑定,避免全局引用:

class ProductService {
  late final Stream<int> timer$;
  late final StreamSubscription<int> sub;

  ProductService() {
    // 创建实例级别的流,随服务销毁而停止
    timer$ = Stream<int>.periodic(const Duration(seconds: 1), (x) => x).asBroadcastStream();
    sub = timer$.listen((int tick) {
      print(tick);
    });
  }

  void onDispose() {
    sub.cancel();
    // 如果是单订阅流,可通过controller关闭;广播流无需额外关闭,但需确保所有订阅都取消
  }
}

关键注意事项

  • 任何订阅了流的对象,必须在生命周期结束时手动取消订阅,尤其是静态流或全局流。
  • 避免让全局静态流持有页面/组件级对象的引用,否则极易引发内存泄漏。

内容的提问来源于stack exchange,提问作者matox

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 23:10:46