【Flutter】内存泄漏总结
一、什么是内存泄漏(Memory Leak)
程序中某些资源(如对象、监听器、句柄等)本该释放但未释放,导致它们一直存在于内存中,即使已经不再使用,最终可能导致内存占满、App 变慢甚至崩溃。

二、Flutter 中常见的内存泄漏类型
类型 |
描述 |
举例 |
未释放监听器 |
添加了监听器没有移除 |
addListener() 、Stream.listen() |
控制器未释放 |
控制器类未调用 dispose() |
TextEditingController 、ScrollController |
动画未停止 |
AnimationController 未释放 |
持续刷新 UI,资源持续占用 |
闭包或回调引用 UI 对象 |
Timer、异步任务中引用了已销毁的 Widget |
异步任务完成后仍尝试调用 UI |
第三方库缓存未清理 |
比如图片库、缓存库未正确释放 |
image_cache、event_bus 等 |
重复创建对象 |
build 中反复创建对象,未缓存或释放 |
每次 build 新建 controller / subscription |
误用 GlobalKey |
使用过多或重复的 GlobalKey 会强引用子 widget |
尤其在 ListView 或复杂 UI 中 |
页面未销毁 |
路由没有 pop / 多层嵌套导致组件卡住 |
自定义路由动画等错误场景 |
三、排查内存泄漏的方法
1. Flutter DevTools – Memory
- 观察内存是否一直上升不下降
- 使用 Heap Snapshot 查看内存中仍然存在的对象
- 通过 Retaining Path 找出是哪里引用了未释放的对象
2. Android Studio / Xcode Instruments
- Android: 使用
Profiler
分析 native 内存使用
- iOS: Instruments 的
Leaks
工具检测未释放对象
3. 打印调试 + 日志
四、具体对象和场景分类整理
1. 控制器类(必须 dispose)
控制器 |
用途 |
必须释放? |
TextEditingController |
文本输入框 |
✅ |
ScrollController |
滚动控制 |
✅ |
AnimationController |
动画驱动 |
✅ |
PageController |
页面控制 |
✅ |
TabController |
标签页控制 |
✅ |
FocusNode / FocusScopeNode |
焦点管理 |
✅ |
2. 订阅监听类
类型 |
描述 |
是否手动取消 |
StreamSubscription |
比如监听网络、事件 |
.cancel() |
ValueNotifier.addListener |
UI 数据绑定 |
.removeListener() 或 .dispose() |
EventBus 事件监听 |
第三方库 |
.cancel() |
ChangeNotifier |
自建或手动监听 |
.dispose() |
3. 异步任务类(潜在泄漏)
类型 |
问题 |
解决 |
Timer |
仍在 tick,UI 已销毁 |
手动 cancel() |
Future |
回调引用了已销毁的 context |
判断 mounted |
async/await 中访问 state |
任务返回太慢 |
加 if (!mounted) return; |
4. 图片缓存泄漏
问题 |
说明 |
处理 |
图片太多占用内存 |
使用了大量 NetworkImage ,未清理 |
清除缓存 imageCache.clear() |
长时间使用 GIF |
会占用大量内存 |
控制播放时长或卸载时释放 |
5. GlobalKey 泄漏
问题 |
原因 |
避免方式 |
重复使用同一个 GlobalKey |
会导致引用未释放 |
避免动态列表中用 GlobalKey ,尽量用 ValueKey |
五、实际案例:常见泄漏代码 + 改进方式
❌ 错误:添加监听后忘记移除
final controller = TextEditingController();
controller.addListener(() {
});
✅ 正确处理
@override
void dispose() {
controller.dispose();
super.dispose();
}
❌ 错误:Future 回调时界面已销毁
@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 3), () {
someStateChange();
});
}
✅ 加 mounted 判断
Future.delayed(Duration(seconds: 3), () {
if (!mounted) return;
someStateChange();
});
六、最佳实践总结
建议 |
说明 |
在 dispose() 中释放所有控制器、订阅对象 |
减少资源长期占用 |
异步回调中使用 if (!mounted) return; |
防止任务晚到 |
避免在 build() 中初始化控制器 |
会导致重复创建 |
对第三方事件、网络监听做取消订阅 |
否则内存长期占用 |
定期使用 DevTools 检查内存曲线和快照 |
早发现泄漏问题 |
控制长图、大图、GIF 使用 |
否则卡顿、OOM |
Widget 树中避免使用过多 GlobalKey |
可能导致 Widget 无法 GC |
七、辅助工具推荐
flutter_hooks
: 自动处理生命周期,简化 dispose()
riverpod
/ provider
: 自动释放状态
DevTools
的 memory 工具
leak_tracker
: 社区维护的内存泄漏检测工具(实验性)
八、关于作者(ZFJ_张福杰)