招贤纳士
前言
目前,Flutter 业务团队反馈最普遍的问题是,Flutter 内存占用过高。
Flutter 内存占用过高原因比较复杂,需另开一个主题才能说清楚。简单总结下我们调研的结论:Dart Heap 内存管理以及 Flutter Widget 设计综合导致业务内存较高,其最核心的问题引擎设计使开发者容易踩中内存泄漏。开发过程中,内存泄漏常见且难以定位,总结主要 2 点原因:
-
Flutter 渲染三棵树的设计,以及 Dart 各种异步编程的特点,导致对象引用关系比较绕,分析困难
-
Dart “闭包”,“实例方法”可赋值传递,导致所在的类被方法上下文持有,不经意就会发生泄漏。典型例如注册一个 listener 没有反注册,导致 listener 所在的类对象泄漏
盘点我了解到的几种内存泄漏检测方案:
-
监控 State 是否泄漏:针对 State 的泄漏检测。但 State 是 Flutter 内存泄漏中占比最大的对象吗?StatelessWidget 的对象也是可以引用很大内存的
-
监控 Layer 个数:对比 正在使用,内存中的 Layer 个数来判定是否存在内存泄漏。方案对内存泄漏判定是否准确?Layer 对象离业务 Widget 太远,溯源太困难
-
Expando 弱引用泄漏判定:判定特定对象是否泄漏并返回引用链 。但我们不知道Flutter 中最应该监控的对象是哪个,哪个对象泄漏是主要问题?
-
基于 Heap Snapshot 内存泄漏检测:对比不同两个时间点的 Dart 虚拟机 Heap 对象的增长,以“class内存增量”,“对象内存个数” 2 个指标检测发生泄漏的可疑对象。这是个通用的解决方案,但要做到高效定位到泄漏对象(Image, Layer)才比较有价值。目前“确定检测对象”和“检测时机”这 2 个问题都不好解决,所以还需要人工逐一排查确认,效率不高。
更好的方案是?
参考 Android,LeakCanary 能够准确、高效检测 Activity 内存泄漏,解决内存泄漏的主要问题。那我们能不能在 Flutter 中也实现一套这样的工具呢?这应该是一套更好的方案。在回答这个问题之前,先思考下为什么 LeakCanary 要挑选 Activity 作为内存泄漏监控的对象,并且能够解决主要的内存泄漏问题?
我们总结其至少满足了下面 3 个条件:
-
泄漏对象引用的内存足够大:Activity 对象引用的内存是非常大,是内存泄漏的主要问题
-
能够完备定义内存泄漏:Activity 具有明确的生命周期和确切回收时机,泄漏定义完备,可实现自动化,提高效率
-
泄漏的风险高:Activity 基类为 Context,作为参数传递,使用非常频繁,存在较高的泄漏风险
顺着这个思路,如果我们能够在 Flutter 中找到满足上面 3 个条件的对象,将其监控起来,那就可以做一套 Flutter 的 LeakCanary 工具,用来解决 Flutter 中内存泄漏的主要问题。
从实际项目中回顾近期解决的内存泄漏问题,内存飙升体现在 Image, Picture 对象,如下图所示。
-
内存占用大,是其对象个数多,累加起来的,并不是由某一个 Image 引用而导致
-
无法定义什么时候是泄漏的,没有明确的生命周期
-
并不会作为一个常用的参数传递,使用地方都比较固定,例如 RawImage Widget
请记住这 3 个条件,后面我们在说明的时候会经常用到。
为什么监控 BuildContext
BuildContext 是 Element 的基类,直接引用 Widget,RenderObject,其类之间的关系也是它们形成的 Element Tree, Widget Tree, RenderObject Tree 的关系。类关系如下图所示。
-
三棵树的构建是通过 Element 的 mount / unmount 方法构建
-
父子 Element 相互强引用, 所以 Element 泄漏会导致整棵 Element Tree 泄漏,连同强引用住对应的 Widget Tree, RenderObject Tree 一起泄漏,相当可观
-
Element 中强引用到 Widget, RenderObject 的 field 不会主动置为 null,所以三棵树的释放依赖 Element 被 GC 回收
RenderObject Tree 会生成 Layer Tree,并且会强引用 ui.EngineLayer(c++ 分配内存),所以 Layer 相关的渲染内存会被这棵树持有。
综合上述,BuildContext 引用住了 Flutter 中的 3 棵树。因此:
-
BuildContext 引用的内存占用大,满足条件 1
-
BuildContext 在业务代码中使用频繁,作为参数传递等,泄漏风险高,满足条件 3
怎么监控 BuildContext
从 Element 的生命周期看:
finalizeTree 处理代码如下:
// flutter_sdk/packages/flutter/lib/src/rendering/binding.dart
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
@override
void drawFrame() {
...
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame();
// 每一帧最后回收从 Element 树中移除的 Element
buildOwner.finalizeTree();
} finally {
}
}
}
// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
class BuildOwner {
...
void finalizeTree() {
try {
// _inactiveElements 中记录不再使用的 Element
lockState(() {
_inactiveElements._unmountAll(); // this unregisters the GlobalKeys
});
} catch() {
}
}
...
}
// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
class _InactiveElements {
...
void _unmountAll() {
_locked = true;
// 将 Element 拷贝到临时变量 elements 中
final List<Element> elements = _elements.toList()..sort(Element._sort);
// 清空 _elements,当前方法执行完,elements 也会被回收,则全部 Element 正常情况下都会被 GC 回收。
_elements.clear();
try {
elements.reversed.forEach(_unmount);
} finally {
assert(_elements.isEmpty);
_locked = false;
}
}
...
}
因此 Element 泄漏可定义为:执行完 umount,并且 GC 后,仍存在这些 Element 的引用,则说明 Element 发生内存泄漏。满足条件 2。
内存泄漏检测工具
工具描述
-
准确。包括核心对象泄漏检测:image, layer,state,能够解决 Flutter 90% 以上对内存泄漏问题
-
高效。业务无感,自动化检测,优化引用链,快速定位到泄漏源
准确
为什么要添加 State 对象的监控呢?
class MainApp extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MainAppState();
}
}
class _MainAppState extends State<MainApp> {
@override
void initState() {
super.initState();
// 注册这个回调,这个回调如果没有被反注册或者被其他上下文持有,都会导致 _MainAppState 泄漏。
xxxxManager.addListerner(handleAction);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
);
}
// 1个回调
void handleAction() {
...
}
}
结合以下代码看,泄漏肯定会导致关联的 Widget 泄漏,而 Widget 关联的内存如果是一张的 Image 或者 gif 的话,泄漏的内存也会很大。同时,State 中可能还以关联其他的一些强引用住的内存。
// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
abstract class State<T extends StatefulWidget> with Diagnosticable {
// 强引用对应的 Widget 泄漏
T _widget;
// unmount 时候,_element = null, 不会导致泄漏
StatefulElement _element;
...
}
// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
class StatefulElement extends ComponentElement {
...
@override
void unmount() {
...
_state.dispose();
_state._element = null;
// 其他地方持有,则导致泄漏。unmount 后 State 仍被持有,可作为一个泄漏定义。
_state = null;
}
...
}
高效
首先我们要怎么明确一个对象是否发生泄漏?以 BuildContext 为例,我们采取类似“Java 对象弱引用”判定对象泄漏的方式:
-
将 finalizeTree 阶段的 inactiveElements 放到 weak Reference map 中
-
Full GC 后检测 weak Reference map ,如果其中仍持有未释放的 Element,则判定为发生泄漏
-
将泄漏的 Element 关联的 size,对应的 Widget,泄漏引用链信息输出
// 添加需要检测泄漏的对象,类似将对象放到若引用map中
external void leakAdd(Object suspect, {
String tag: '',
});
// 检测之前放入的对象是否发生了泄漏,会进行 FullGc
external void leakCheck({
Object? callback,
String tag: '',
bool clear: true,
});
external void leakClear({
String tag: '',
});
external String leakCount();
external List<String> leakTags();
leakAdd 时机
// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
class _InactiveElements {
...
void _unmount(Element element) {
element.visitChildren((Element child) {
assert(child._parent == element);
_unmount(child);
});
// BuildContext 泄漏 leakAdd() 时机
if (!kReleaseMode && debugMemoryLeakCheckEnabled && null != debugLeakAddCallback) {
debugLeakAddCallback(_state);
}
element.unmount();
...
}
...
}
// flutter_sdk/packages/flutter/lib/src/widgets/framework.dart
class StatefulElement extends ComponentElement {
@override
void unmount() {
_state.dispose();
_state._element = null;
// State 泄漏 leakAdd() 时机
if (!kReleaseMode && debugMemoryLeakCheckEnabled && null != debugLeakAddCallback) {
debugLeakAddCallback(_state);
}
_state = null;
}
}
leakCheck 时机
// flutter_sdk/packages/flutter/lib/src/widgets/navigator.dart
abstract class Route<T> {
_navigator = null;
// BuilContext, State leakCheck时机
if (!kReleaseMode && debugMemoryLeakCheckEnabled && null != debugLeakCheckCallback) {
debugLeakCheckCallback();
}
}
工具实现
-
Hummer 引擎深度定制的 DevTools 资源面板展示,可以自动/手动触发内存泄漏检测
-
独立 APP 端内存泄漏展示,在 Page 发生泄漏时候,弹出泄漏对象详情
-
Hummer 引擎海鸥实验室自动化检测,自动化将内存泄漏详情以报告给出
异常检测实例
// 验证 StatelessWidget 泄漏
class StatelessImageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 模拟静态持有 BuildContext 导致泄漏
MyApp.sBuildContext.add(context);
return Center(
child: Image(
image: NetworkImage("https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
width: 200.0,
)
);
}
}
class StatefulImageWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _StatefulImageWidgetState();
}
}
// 验证 StatefulWidget 泄漏
class _StatefulImageWidgetState extends State<StatefulImageWidget> {
@override
Widget build(BuildContext context) {
if (context is ComponentElement) {
print("sBuildContext add :" + context.widget.toString());
}
// 模拟被 Timer 异步持有 BuildContext 导致泄漏,延时 1h 用于说明问题
Timer(Duration(seconds: 60 * 60), () {
print("zw context:" + context.toString());
});
return Center(
child: Image(
image: NetworkImage("https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
width: 200.0,
)
);
}
}
工具 1 - DevTools 资源面板展示:
聚合页展示所有泄漏对象,详情页展示了泄漏的对象以及对象引用链。
业务实战
这个例子中泄漏的 StatefulElent 对应的是一个重量级页面,Element Tree 非常深,关联泄漏的内存很可观。我们解决这个问题后,业务由于 OOM 导致的崩溃率下降显著。
业务同学对此非常认可,这也给了我们做这套工具很大的鼓舞,因为可以快速解决实际的问题,赋能业务。
总结展望
-
更准确:包括核心泄漏对象 widget,Layer,State;直接监控泄漏的根源;完备定义内存泄漏
-
更高效:自动化检测泄漏对象,更加短和直接的引用链
-
业务无感知:减轻开发负担
该方案可以覆盖我们当前遇到所有的内存泄漏问题,大大提升内存泄漏检测效率,为我们业务 Flutter 化保驾护航。目前方案实现基于 Hummer 引擎,运行在 debug,profile模式下,后续会探索线上 release 模式检测,覆盖本地无法复现的场景。
U4 内核致力于打造性能最好、最安全的 web 平台,让 web 无所不能。
关注公众号请搜索 U4内核技术,即时获取最新的技术动态