回顾 24年 Flutter 骨架屏没有释放 CurvedAnimation 导致内存泄漏的血案

📋 问题概述

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

内存泄漏原因

  1. CurvedAnimation 没有保存引用:创建后无法手动释放
  2. Animation 对象持有强引用:对 AnimationController 形成强引用链
  3. Widget 销毁时未释放:CurvedAnimation 没有被正确释放
  4. 循环引用: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

后来我们也陆续参与的其他开源组件修复

等等...这说明我们一不留意就忽视了手动释放

🎯 最佳实践

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. 内存占用持续增长:每次创建组件都会泄漏内存
  2. 应用性能下降:内存不足时触发垃圾回收
  3. 用户体验恶化:页面卡顿、响应缓慢
  4. 系统资源浪费:影响其他应用性能

修复后的效果

  1. 内存使用稳定:不再持续增长
  2. 应用性能提升:减少垃圾回收频率
  3. 用户体验改善:页面流畅度提升
  4. 系统资源优化:更好的资源利用

🔧 预防措施

1. 代码审查

  • 检查所有 Animation 对象的生命周期管理
  • 确保 dispose() 方法正确实现
  • 验证引用是否正确释放

2. 自动化测试

  • 添加内存泄漏检测测试
  • 集成性能监控工具
  • 定期进行压力测试

3. 开发规范

  • 建立组件生命周期管理规范
  • 使用 lint 规则检查常见问题
  • 定期进行代码质量审查

📝 总结

CurvedAnimation 内存泄漏是一个容易被忽视但影响严重的问题。通过正确的引用管理和生命周期控制,可以有效避免这类问题。关键是要记住:

  1. 所有 Animation 对象都要保存引用
  2. 在 dispose() 中按正确顺序释放
  3. 释放后可以将引用设置为 null
  4. 使用 mounted 检查避免在已销毁的 Widget 上操作

写这篇文章的目的是提醒我们在 Flutter 开发中要特别注意特殊的对象内存管理,避免因为疏忽导致的内存泄漏问题。另外推荐下另外一篇做法 Flutter应用使用leak_tracker监控内存泄露 确实能有效的监控泄漏

相关推荐
柯北(jvxiao)15 分钟前
Vue vs React 多维度剖析: 哪一个更适合大型项目?
前端·vue·react
JefferyXZF28 分钟前
Next.js 中间件:掌握请求拦截与处理的核心机制(六)
前端·全栈·next.js
知识分享小能手35 分钟前
Vue3 学习教程,从入门到精通,Vue 3 + Tailwind CSS 全面知识点与案例详解(31)
前端·javascript·css·vue.js·学习·typescript·vue3
石小石Orz1 小时前
React生态蓝图梳理:前端、全栈与跨平台全景指南
前端
袁煦丞1 小时前
8.12实验室 指尖魔法变出艺术感 Excalidraw:cpolar内网穿透实验室第495个成功挑战
前端·程序员·远程工作
烛阴2 小时前
Dot
前端·webgl
Gene_20222 小时前
使用行为树控制机器人(三) ——通用端口
前端·机器人
excel2 小时前
JavaScript 中的二进制数据:ArrayBuffer 与 SharedArrayBuffer 全面解析
前端
来来走走2 小时前
Flutter SharedPreferences存储数据基本使用
android·flutter
ZXT3 小时前
代码规范与提交
前端