📋 问题概述
在 Flutter 应用开发中,骨架屏是一个常用的 UI 组件,用于在数据加载时提供视觉反馈。然而,如果不正确处理动画对象的生命周期,会导致严重的内存泄漏问题,最终引发应用卡顿。而我们踩这个坑是发生在去年,我这里做了记录回顾一下案情过程~
🚨 血案回顾
1. 问题现象
- App 使用一段时间后页面变得卡顿
- 内存占用持续增长
- 骨架屏动画越多,问题越严重
- 用户反馈应用越来越慢
2. 排查过程
- 1、团队排查很久没有发现问题 & 有伙伴提出可能是骨架屏动画较多导致? 我们的封装是参考了此篇文章的 link 因此也不觉得有什么不对。
- 2、后来在我给官方 Flutter 提交一个修复 PR 合并后才被检测出内存泄漏。后来官方人员也提了 Pr 解决了问题!
- 3、这说明是很容易被大家忽视的,即使是官方人员的审核~
- 4、此时联系起应用中的卡顿问题 😂😂😂
🔍 根本原因分析
问题代码示例
dart
// ❌ 错误实现 - 存在内存泄漏
gradientPosition = Tween<double>(
begin: -3,
end: 10,
).animate(
CurvedAnimation(parent: controller!, curve: Curves.linear), // 没有保存引用
)..addListener(() {
if (mounted) {
setState(() {});
}
});
内存泄漏原因
- CurvedAnimation 没有保存引用:创建后无法手动释放
- Animation 对象持有强引用:对 AnimationController 形成强引用链
- Widget 销毁时未释放:CurvedAnimation 没有被正确释放
- 循环引用:Animation 和 Controller 之间形成循环引用
🛠️ 修复方案
正确实现
dart
class TWSkeletonViewState extends State<TWSkeletonView>
with SingleTickerProviderStateMixin {
AnimationController? controller;
Animation? gradientPosition;
// ✅ 添加 CurvedAnimation 引用
CurvedAnimation? curvedAnimation;
@override
void initState() {
super.initState();
// ... 其他初始化代码
if (controller != null) {
// ✅ 保存 CurvedAnimation 引用
curvedAnimation = CurvedAnimation(
parent: controller!,
curve: Curves.linear
);
gradientPosition = Tween<double>(
begin: -3,
end: 10,
).animate(curvedAnimation!)
..addListener(() {
if (mounted) {
setState(() {});
}
});
}
}
@override
void dispose() {
// ✅ 手动释放 CurvedAnimation
curvedAnimation?.dispose();
curvedAnimation = null;
controller?.dispose();
controller = null;
super.dispose();
}
}
📚 相关资源
Flutter 官方的 issue 说明他们一直在优化框架
阶段1 Clean up notDisposed leaks in Flutter Framework, phase 1.
阶段2 Clean up memory leaks in Flutter Framework, phase 2.
Classes to instrument
- RouteEntry
- AnimationController
- AnimationEagerListenerMixin
- BannerPainter
- ChangeNotifier
- CurvedAnimation
- DisposableBuildContext
- Element
- GestureRecognizer
- Image
- ImageInfo
- ImageStreamCompleterHandle
- Layer
- MultiDragPointerState
- OverlayEntry
- PerformanceModeRequestHandle
- Picture
- PipelineOwner
- RenderObject
- RestorationBucket
- Route
- ScrollDragController
- SelectionOverlay
- SemanticOwner
- SnapshotPainter
- State
- TextPainter
- TextSelectionOverlay
阶段3 Clean up 'not disposed' memory leaks in FF, phase 3
- AnimationEagerListenerMixin
- CurvedAnimation
- TrainHoppingAnimation
- ImageInfo
- BoxPainter
- ScrollDragController
- SelectionOverlay
- SnapshotPainter
- TextSelectionOverlay
后来我们也陆续参与的其他开源组件修复
- fvp #267 by @lxf
- flutter_carousel_slider #488 by @hello-coder-xu
- flutter_wechat_assets_picker #709
- flutter_smart_dialog #272
等等...这说明我们一不留意就忽视了手动释放
🎯 最佳实践
1. 引用管理
dart
// ✅ 保存所有需要释放的引用
late AnimationController controller;
CurvedAnimation? curvedAnimation;
StreamSubscription? subscription;
Timer? timer;
2. 释放顺序
dart
@override
void dispose() {
timer?.cancel();
timer = null;
subscription?.cancel();
subscription = null;
curvedAnimation?.dispose();
curvedAnimation = null;
controller.dispose();
super.dispose();
}
3. 安全检查
dart
// ✅ 使用 mounted 检查
if (mounted) {
setState(() {});
}
// ✅ 使用 ?. 操作符
curvedAnimation?.dispose();
4. 内存监控
dart
// ✅ 在开发阶段监控内存使用
import 'dart:developer' as developer;
void logMemoryUsage() {
developer.log('Memory usage: ${ProcessInfo.currentRss}');
}
🧪 测试方法
1. 内存泄漏测试
dart
// 创建大量组件
List<TWSkeletonView> skeletons = List.generate(
1000,
(index) => TWSkeletonView()
);
// 重复创建和销毁
for (int i = 0; i < 100; i++) {
setState(() {
skeletons = List.generate(1000, (index) => TWSkeletonView());
});
}
2. 性能监控
- 使用 Flutter Inspector 监控内存使用
- 观察内存增长趋势
- 检查是否有内存泄漏警告
📈 影响评估
内存泄漏的影响
- 内存占用持续增长:每次创建组件都会泄漏内存
- 应用性能下降:内存不足时触发垃圾回收
- 用户体验恶化:页面卡顿、响应缓慢
- 系统资源浪费:影响其他应用性能
修复后的效果
- 内存使用稳定:不再持续增长
- 应用性能提升:减少垃圾回收频率
- 用户体验改善:页面流畅度提升
- 系统资源优化:更好的资源利用
🔧 预防措施
1. 代码审查
- 检查所有 Animation 对象的生命周期管理
- 确保 dispose() 方法正确实现
- 验证引用是否正确释放
2. 自动化测试
- 添加内存泄漏检测测试
- 集成性能监控工具
- 定期进行压力测试
3. 开发规范
- 建立组件生命周期管理规范
- 使用 lint 规则检查常见问题
- 定期进行代码质量审查
📝 总结
CurvedAnimation 内存泄漏是一个容易被忽视但影响严重的问题。通过正确的引用管理和生命周期控制,可以有效避免这类问题。关键是要记住:
- 所有 Animation 对象都要保存引用
- 在 dispose() 中按正确顺序释放
- 释放后可以将引用设置为 null
- 使用 mounted 检查避免在已销毁的 Widget 上操作
写这篇文章的目的是提醒我们在 Flutter 开发中要特别注意特殊的对象内存管理,避免因为疏忽导致的内存泄漏问题。另外推荐下另外一篇做法 Flutter应用使用leak_tracker监控内存泄露 确实能有效的监控泄漏