简介
应用内存包含 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 验证是否存在泄漏
一个实用的排查过程是:
- 在固定设备和构建模式下记录基线快照。
- 按相同步骤进入和退出可疑页面多次。
- 等待必要的异步任务结束,并在 DevTools Memory 视图中对比 Diff Snapshots。
- 检查特定类的实例数量、外部内存变化和 retaining path。
- 如果对象仍被不需要的长期引用持有,再定位全局字段、监听器、闭包或插件资源。
一次曲线没有回落不能证明泄漏。垃圾回收时机、图片缓存、原生分配和仍然可达的对象都会影响观测值;持续增长需要结合快照和引用路径进一步确认。
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 订阅的取消。但没有留下可复现的前后内存快照和设备数据,因此本文不声称某个改动已量化降低内存,也不把经验外推为所有应用的结论。
