跳转到正文
YGJoon 技术博客

Flutter 内存泄漏怎么查?Timer、Stream、图片缓存与 DevTools

发布于:
·1.2k 字 · 约 3 分钟·作者:
从 Timer、Stream 和 AnimationController 生命周期入手,配合图片解码尺寸与 DevTools 快照定位 Flutter 内存问题。
Flutter 内存泄漏怎么查?Timer、Stream、图片缓存与 DevTools文章封面
目录

简介

应用内存包含 Dart heap、图片与纹理、插件和原生对象等部分。单看 RSS 或某一时刻的 Dart heap,不能直接判断应用是否泄漏;先明确问题发生在哪类内存,再用快照和 retaining path 查找仍被引用的对象。

1. 让资源跟随 Widget 生命周期

AnimationController 需要 TickerProvider;StreamSubscription 与 Timer 也必须保存并在 owner 销毁时取消。下面的组件展示这些资源由同一个 State 管理:

import 'dart:async';

import 'package:flutter/material.dart';

class ResourceOwner extends StatefulWidget {
  const ResourceOwner({
    super.key,
    required this.events,
  });

  final Stream<int> events;

  @override
  State<ResourceOwner> createState() => _ResourceOwnerState();
}

class _ResourceOwnerState extends State<ResourceOwner>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  StreamSubscription<int>? _subscription;
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    _subscription = widget.events.listen(_handleEvent);
    _timer = Timer.periodic(
      const Duration(seconds: 5),
      _handleTimer,
    );
  }

  void _handleEvent(int event) {
    // 更新由当前页面拥有的状态。
  }

  void _handleTimer(Timer timer) {
    // 执行确实需要周期调度的任务。
  }

  @override
  Widget build(BuildContext context) {
    return const SizedBox.shrink();
  }

  @override
  void dispose() {
    _timer?.cancel();
    unawaited(_subscription?.cancel());
    _controller.dispose();
    super.dispose();
  }
}

若资源由服务对象持有,也应让服务提供明确的 start/stop/dispose 契约,并在服务宿主销毁时调用。不要让静态 Timer 或订阅的生命周期长于它所服务的页面。

2. 按显示尺寸解码图片

超出显示尺寸很多的图片会增加解码和图像内存开销。对固定布局可以为 Image 提供接近设备像素尺寸的 cacheWidth / cacheHeight;显示尺寸、设备像素比和裁切方式都要一起考虑。

Image.network(
  imageUrl,
  width: 180,
  height: 120,
  fit: BoxFit.cover,
  cacheWidth: 360,
  cacheHeight: 240,
  errorBuilder: (context, error, stackTrace) {
    return const Icon(Icons.broken_image);
  },
)

这只是 2 倍像素密度的示意值,并非所有设备的通用最优值。滚动列表通常应优先懒加载;只对首屏必需的少量资源考虑预加载:

Future<void> precachePageImage(
  BuildContext context,
  String assetPath,
) {
  return precacheImage(AssetImage(assetPath), context);
}

不要对长列表全量 precache。大量尚未显示的图片会提前占用解码后内存。

3. 谨慎调整图片缓存

Flutter 的 ImageCache 管理图片 provider 的缓存。全局修改最大缓存数量会影响整站图片复用;先用 Inspector 检查 oversized images,并观测图片密集场景的内存曲线,再考虑调整应用级缓存策略。

不要把 clearLiveImages() 当成内存压力释放接口。Flutter API 明确说明该方法不会减轻内存压力;它清除的是缓存记录,而 live image 仍可能被其他对象引用。页面退出时应先释放自己的 Image、provider 和引用,必要时通过快照确认对象是否仍可达。

4. 用 DevTools 验证是否存在泄漏

一个实用的排查过程是:

  1. 在固定设备和构建模式下记录基线快照。
  2. 按相同步骤进入和退出可疑页面多次。
  3. 等待必要的异步任务结束,并在 DevTools Memory 视图中对比 Diff Snapshots。
  4. 检查特定类的实例数量、外部内存变化和 retaining path。
  5. 如果对象仍被不需要的长期引用持有,再定位全局字段、监听器、闭包或插件资源。

一次曲线没有回落不能证明泄漏。垃圾回收时机、图片缓存、原生分配和仍然可达的对象都会影响观测值;持续增长需要结合快照和引用路径进一步确认。

5. 采样 RSS 时保留并取消 Timer

ProcessInfo.currentRss 是原生进程级数据,不适用于 Flutter Web,也不等于 Dart heap。下面的示例保留 Timer,使停止行为可控:

import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';

class RssLogger {
  Timer? _timer;

  void start() {
    if (_timer != null) return;
    _timer = Timer.periodic(
      const Duration(seconds: 5),
      (_) {
        if (kDebugMode) {
          final rssMb = ProcessInfo.currentRss / (1024 * 1024);
          debugPrint('Process RSS: ${rssMb.toStringAsFixed(1)} MB');
        }
      },
    );
  }

  void stop() {
    _timer?.cancel();
    _timer = null;
  }
}

生产环境不要每几秒打印或同步上传进程数据。需要线上监控时,应先定义采样率、隐私边界和平台覆盖,再以异步方式采集。

实战记录

我在旅行类项目中使用过图片缓存,并在 Flutter 插件项目里重点检查过 Stream 订阅的取消。但没有留下可复现的前后内存快照和设备数据,因此本文不声称某个改动已量化降低内存,也不把经验外推为所有应用的结论。

参考资料


分享这篇文章:


相关文章

上一篇
Flutter 动画掉帧排查:RepaintBoundary 到底该不该加
下一篇
Flutter 包为什么越来越大?用 analyze-size 找出真正占空间的内容