Flutter memory leak 事例分析memory leak是什么 Dart的垃圾回收算法和Java一样是分代
memory leak是什么
Dart的垃圾回收算法和Java一样是分代垃圾回收算法。判断一个对象是否是垃圾,是否需要回收,按照可达性分析算法标记回收。从Root对象到一个对象之间有链路连接,代表这个对象是可用状态,不可以被回收;从Root对象到一个对象之间没有链路连接,也就是说这个对象和Root对象之间的链路断开了,代表这个对象已经不可用,可以被标记为垃圾,可以被回收了。内存泄漏是本该回收的对象,因为编码错误,导致对象不能回收。垃圾回收器能够从根本上避免内存泄漏吗?垃圾回收器有垃圾回收功能,就避免不了内存泄漏,垃圾回收和内存泄漏是没有交集的两种情况。
memory leak危害
内存泄漏是因为本应该回收的内存,因为编码错误或者编程不规范引起的bug。内存不正常回收,如果多次内存泄漏导致内存占用过高,内存占用过高导致GC频繁执行,导致应用变得卡顿,甚至出现应用不响应的情况。如果内存泄漏过大会出现内存使用大小超出操作系统分配的内存大小而导致的OOM问题引起应用崩溃。内存泄漏可能导致调用已经回收对象,引起应用崩溃。
内存泄漏事例一
页面中有耗时任务,在耗时任务执行完成之前退出当前页面,引起内存泄漏。业务场景是页面一跳转到页面二,页面二耗时任务执行完成之前,关闭页面二。 页面一的示例代码如下:
import 'package:batterylevel/home_second.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
home: HomePage(),
));
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text("home"),
centerTitle: true,
backgroundColor: Colors.black12,
),
body: Center(
child: ElevatedButton(
onPressed: () => {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const UserPage()))
},
child: const Text("click me"),
),
),
),
);
}
}
页面二的示例代码如下:
import 'package:flutter/material.dart';
class UserPage extends StatefulWidget {
const UserPage({super.key});
@override
State<UserPage> createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
List<Map<String, dynamic>> data = [];
bool loading = false;
@override
void initState() {
super.initState();
initData();
}
void initData() async {
setState(() {
loading = true;
});
data = await fakeApi();
setState(() {
loading = false;
});
}
Future<List<Map<String, dynamic>>> fakeApi() async {
await Future.delayed(const Duration(seconds: 5));
return [
{'name': "caicai"},
{'name': "Dart"},
{'name': "Flutter"},
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Mode"),
),
body: loading
? const Center(
child: CircularProgressIndicator(),
)
: ListView.separated(
itemBuilder: (context, index) {
Map<String, dynamic> map = data[index];
return ListTile(
title: Text(map['name']),
);
},
separatorBuilder: (context, index) {
return const Divider();
},
itemCount: data.length),
);
}
}
运行效果如下:
文案翻译如下:
FlutterError (dispose() 之后调用 setState():_UserPageState#fc39f(生命周期状态:defunct,not mounted)
如果您对不再出现在窗口小部件树中的窗口小部件(例如,其父窗口小部件不再在其构建中包含该窗口小部件)的 State 对象调用 setState(),则会发生此错误。当代码从计时器或动画回调中调用 setState() 时,可能会发生此错误。
首选解决方案是取消计时器或停止在 dispose()回调中监听动画。另一个解决方案是在调用 setState() 之前检查此对象的“mounted”属性,以确保对象仍在树中。
如果正在调用 setState(),则此错误可能表示内存泄漏,因为另一个对象在从树中删除此 State 对象后仍保留对该对象的引用。为避免内存泄漏,请考虑在 dispose() 期间中断对此对象的引用。)
Android内存泄漏监测使用LeakCancary,Flutter开发直接在IDE中直接显示,这种方式更加直接,更加高效。
解决方案是在_UserPageState重写setState方法,如果mounted则调用父类的方法。 mounted在dispose()方法被调用之后会设置为false,所以在dispose()返回之后不会调用super.setState(fn);
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
注意以下几点:
- 始终在State或StatefulWidget的dispose()方法中处置AnimationController、ScrollController、TextEditingController等控制器,调用他们的dispose()方法(注意空安全)。
- 如果开启了Timer,请确保在重写dispose()方法时候取消Timer。
- 如果您订阅监听了Stream,请确保在重写dispose()方法时候取消订阅Stream。
- 避免将BuildContext传递给可能比小部件存活时间更长的异步操作。如果必须传递上下文,请考虑使用 mounted来检查小部件是否仍在小部件树中,然后再使用上下文。
- 谨慎使用全局变量和静态字段。确保它们不包含对应该被垃圾回收的对象的引用。
内存泄漏事例二
示例代码如下:
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
void initState() {
super.initState();
_asyncOperation();
}
Future<void> _asyncOperation() async {
await Future.delayed(const Duration(seconds: 2));
// Using `context` here after async operation can lead to a memory leak
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Hello!')));
}
@override
Widget build(BuildContext context) {
return Container();
}
}
异常信息如下图所示:
解决方案如下面代码所示,更改_asyncOperation()方法:
Future<void> _asyncOperation() async {
await Future.delayed(const Duration(seconds: 2));
// Using `context` here after async operation can lead to a memory leak
if (!mounted) {
return;
}
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Hello!')));
}
内存泄漏事例三
内存泄漏的根本原因是长寿命的对象持有短寿命对象的引用,上面两个事例都是这个原因。 如果使用单例或在整个应用程序生命周期内生存的对象,请谨慎处理这些对象保留的内容。避免对短寿命对象进行不必要的引用,导致短寿命对象不能被回收,引起内存泄漏。
内存泄漏事例四
对于不应阻止垃圾收集的对象,如果您需要维护引用但希望在不存在其他强引用时对该对象进行垃圾收集,请考虑使用 WeakReference。需要判断WeakReference的value值是否为null,需要做空安全的相应处理。WeakReference也可以用于占用内存比较大,内存使用情况敏感的情况,比如说用于缓存。
总结
富兰克林说过一句话:A Small Leak Will Sink a Great Ship。中国也有句老话:千里之堤,溃于蚁穴。内存泄漏看似问题很小,但是影响显著。可能会引起卡顿,应用不反应,甚至崩溃等问题。所以我们需要花精力来修复解决这些相关的问题。希望文章对你有帮助,如果发现文章的纰漏,请您不吝赐教。
转载自:https://juejin.cn/post/7406238334216159267