关于flutter:这可能是Flutter-中最强悍的内存泄漏检测方案

7次阅读

共计 8818 个字符,预计需要花费 23 分钟才能阅读完成。

作者:吴志伟

近两年来,无论是创新型利用还是老牌旗舰型利用,都在或多或少地应用 Flutter 技术。然而,目前 Flutter 业务团队反馈最广泛的问题是,Flutter 内存占用过高

Flutter 内存占用过高起因比较复杂,需另开一个主题能力说分明。简略总结下咱们调研的论断:Dart Heap 内存治理以及 Flutter Widget 设计综合导致业务内存较高,其最外围的问题引擎设计使开发者容易踩中 内存透露。开发过程中,内存透露常见且难以定位,总结次要 2 点起因:

  • Flutter 渲染三棵树的设计,以及 Dart 各种异步编程的特点,导致对象援用关系比拟绕,剖析艰难
  • Dart“闭包”,“实例办法”可赋值传递,导致所在的类被办法上下文持有,不经意就会产生透露。典型例如注册一个 listener 没有反注册,导致 listener 所在的类对象透露

开发者享受了 Flutter 开发的便利性,却人不知; 鬼不觉中接受了内存透露的苦果。因而,咱们迫切需要一套高效的内存透露检测工具来解脱这种窘境。

盘点我理解到的几种内存透露检测计划:

  1. 监控 State 是否透露:针对 State 的透露检测。但 State 是 Flutter 内存透露中占比最大的对象吗?StatelessWidget 的对象也是能够援用很大内存的
  2. 监控 Layer 个数:比照 正在应用,内存中的 Layer 个数来断定是否存在内存透露。计划对内存透露断定是否精确?Layer 对象离业务 Widget 太远,溯源太艰难
  3. Expando 弱援用透露断定:断定特定对象是否透露并返回援用链。但咱们不晓得 Flutter 中最应该监控的对象是哪个,哪个对象透露是次要问题?
  4. 基于 Heap Snapshot 内存透露检测:比照不同两个工夫点的 Dart 虚拟机 Heap 对象的增长,以“class 内存增量”,“对象内存个数”2 个指标检测产生透露的可疑对象。这是个通用的解决方案,但要做到高效定位到透露对象(Image, Layer)才比拟有价值。目前“确定检测对象”和“检测机会”这 2 个问题都不好解决,所以还须要人工逐个排查确认,效率不高。

总之,咱们感觉计划 1,2 逻辑上不够齐备,计划 3,4 效率有待进步。

更好的计划是?

参考 Android,LeakCanary 可能精确、高效检测 Activity 内存透露,解决内存透露的次要问题。那咱们能不能在 Flutter 中也实现一套这样的工具呢?这应该是一套更好的计划。

在答复这个问题之前,先思考下为什么 LeakCanary 要筛选 Activity 作为内存透露监控的对象,并且可能解决次要的内存透露问题?

咱们总结其至多满足了上面 3 个条件:

  1. 透露对象援用的内存足够大:Activity 对象援用的内存是十分大,是内存透露的次要问题
  2. 可能齐备定义内存透露:Activity 具备明确的生命周期和确切回收机会,透露定义齐备,可实现自动化,提高效率
  3. 透露的危险高:Activity 基类为 Context,作为参数传递,应用十分频繁,存在较高的透露危险

3 个条件反映了监控对象的必要性,监控工具的可操作性。

顺着这个思路,如果咱们可能在 Flutter 中找到满足下面 3 个条件的对象,将其监控起来,那就能够做一套 Flutter 的 LeakCanary 工具,用来解决 Flutter 中内存透露的次要问题。

从理论我的项目中回顾近期解决的内存透露问题,内存飙升体现在 Image, Picture 对象,如下图所示。

尽管 Image, Picture 内存占用高,是透露内存的次要贡献者,但它们不能作为咱们监控的指标,因为它们显著不合乎下面列出的 3 个条件:

  1. 内存占用大,是其对象个数多,累加起来的,并不是由某一个 Image 援用而导致
  2. 无奈定义什么时候是透露的,没有明确的生命周期
  3. 并不会作为一个罕用的参数传递,应用中央都比拟固定,例如 RawImage Widget

深刻 Flutter 渲染剖析,总结到 Image, Picture 透露的根本原因是 BuildContext 产生透露。而 BuildContext 恰好满足下面列的 3 个条件(前面详述),仿佛是咱们要找的那个对象,实现一套监控 BuildContex 透露的计划仿佛不错。

请记住这 3 个条件,前面咱们在阐明的时候会常常用到。

为什么监控 BuildContext

BuildContext 援用的内存有哪些呢?

BuildContext 是 Element 的基类,间接援用 Widget,RenderObject,其类之间的关系也是它们造成的 Element Tree, Widget Tree, RenderObject Tree 的关系。类关系如下图所示。

[]()

着重说下 Element Tree:

  • 三棵树的构建是通过 Element 的 mount / unmount 办法构建
  • 父子 Element 互相强援用, 所以 Element 透露会导致整棵 Element Tree 透露,连同强援用住对应的 Widget Tree, RenderObject Tree 一起透露,相当可观
  • Element 中强援用到 Widget, RenderObject 的 field 不会被动置为 null,所以三棵树的开释依赖 Element 被 GC 回收

Widget Tree 示意被援用的 Widget,例如援用 Image 的 RawImage Widget。RenderObject Tree 会生成 Layer Tree,并且会强援用 ui.EngineLayer(c++ 分配内存),所以 Layer 相干的渲染内存会被这棵树持有。综合上述,BuildContext 援用住了 Flutter 中的 3 棵树。因而:

  1. BuildContext 援用的内存占用大,满足条件 1
  2. BuildContext 在业务代码中应用频繁,作为参数传递等,透露危险高,满足条件 3

怎么监控 BuildContext

BuildContext 的透露是否能够齐备定义?

从 Element 的生命周期看:

重点须要确定什么时候 Element 会被 Element Tree 抛弃,并且不会再应用,会被随后来的 GC 回收掉。

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;
    }
  }
  ...
}

finalize 阶段 _inactiveElements 中保留了被 Element Tree 抛弃,并且不会再应用的 Element;在执行完 unmount 办法后,即期待被 GC 回收。

因而 Element 透露可定义为:执行完 umount,并且 GC 后,仍存在这些 Element 的援用,则阐明 Element 产生内存透露。满足条件 2。

内存透露检测工具

工具形容

咱们对内存透露工具有 2 点要求:

  1. 精确。包含外围对象透露检测:image, layer,state,可能解决 Flutter 90% 以上对内存透露问题
  2. 高效。业务无感,自动化检测,优化援用链,疾速定位到透露源

精确

从上文形容,BuildContext 毫无疑问是最有可能导致大内存透露的对象,是作为监控对象的最佳对象。为了进步准确度,咱们也把最罕用的 State 对象监控起来。

为什么要增加 State 对象的监控呢?

因为业务逻辑管制实现在 State 中,业务中实现的“闭包或者办法”传递很容易导致 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() {...}
}

State 关联哪些内存会被透露?

联合以下代码看,透露必定会导致关联的 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,业务常操作的 State 一并监控起来,进步整套计划的准确度。

高效

怎么实现自动化高效的内存透露检测?

首先咱们要怎么明确一个对象是否产生透露?以 BuildContext 为例,咱们采取相似“Java 对象弱援用”断定对象透露的形式:

  1. 将 finalizeTree 阶段的 inactiveElements 放到 weak Reference map 中
  2. Full GC 后检测 weak Reference map,如果其中仍持有未开释的 Element,则断定为产生透露
  3. 将透露的 Element 关联的 size,对应的 Widget,透露援用链信息输入

尽管 Dart 没有间接提供“弱援用”检测能力,但咱们 Hummer 引擎从底层将“弱援用透露检测”性能残缺实现了,这里简略介绍它断定透露的接口:

// 增加须要检测透露的对象,相似将对象放到若援用 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(),leakCheck() 调用的机会即可。

leakAdd 机会

BuildContext 的机会在 finalizeTree 的 unmount 流程中:

// 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();
    ...
  }
  ...
}

State 的机会在对应的 StatefulElement 的 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 机会

leakCheck 实质上是一个检测是否存在透露的机会点,咱们认为 Page 退出是个适合的机会,以业务 Page 为单位进行内存透露检测。示例代码如下:

// flutter_sdk/packages/flutter/lib/src/widgets/navigator.dart
abstract class Route<T> {
  _navigator = null;
  // BuilContext, State leakCheck 机会
  if (!kReleaseMode && debugMemoryLeakCheckEnabled && null != debugLeakCheckCallback) {debugLeakCheckCallback();
  }
} 

工具实现

以 Page 为单位的自动化内存透露,依据应用场景,提供三种内存透露检测工具。

  1. Hummer 引擎深度定制的 DevTools 资源面板展现,能够主动 / 手动触发内存透露检测
  2. 独立 APP 端内存透露展现,在 Page 产生透露时候,弹出透露对象详情
  3. Hummer 引擎海鸥实验室自动化检测,自动化将内存透露详情以报告给出

工具 1、2 提供开发过程的内存透露检测能力,工具 3 可作为 APP 惯例衰弱测试,自动化测试并输入检测报告后果。

异样检测实例

在 Demo 中模仿 StatelessWidget, StatefulWidget 被 BuildContext 持有导致的透露。透露的起因是被动态持有,Timer 异样持有。

// 验证 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,
        )
    );
  }
}

别离进入 2 个 Widget 页面退出,检测透露后果。

工具 1 – DevTools 资源面板展现:

StatefulElement 透露检测,可见 StatefulImageWidget 被 Timer 异步持有导致透露。

StatelessElement 透露检测,可见 StatelessImageWidget 被动态持有导致导致透露。

工具 2 – 独立 app 端透露展现:

聚合页展现所有透露对象,详情页展现了透露的对象以及对象援用链。

依据工具给出的透露链,都可能疾速地找到透露源。

业务实战

UC 某个内容型业务,特点是多图文、视频内容,内存耗费相当大。之前咱们基于 Flutter 原生 Observatory 工具解决了一些 State, BuildContext 透露问题(耗时漫长,相当苦楚)。为了验证工具的实用价值,咱们将内存透露问题还原去验证。后果发现:之前苦苦排查的问题,霎时就能检测进去,效率大大提高,与 Observatory 工具去排查比照,几乎是云泥之别。基于新的工具,咱们陆续发现了许多之前没有排查进去的内存透露问题。

这个例子中透露的 StatefulElent 对应的是一个重量级页面,Element Tree 十分深,关联透露的内存很可观。咱们解决这个问题后,业务因为 OOM 导致的解体率降落显著。

咱们的另一款纯 Flutter APP 的开发同学反馈,晓得局部场景下内存会减少,存在透露,但没有无效的伎俩进行检测和解决。接入咱们的工具进行检测,后果检测出多处不同场景下的内存透露问题。

业务同学对此十分认可,这也给了咱们做这套工具很大的鼓励,因为能够疾速解决理论的问题,赋能业务。

总结瞻望

从 Flutter 内存透露的理论登程,总结了内存耗费的大头次要是 Image, Layer 以及摸索一套高效内存透露检测计划的必要性。通过借鉴 Android 的 leak-canary,咱们总结了寻找透露监控对象的三个条件;通过对 Flutter 渲染三棵树的剖析,确定 BuildContext 作为监控对象。为了进步检测工具的准确性,咱们又减少 State 的监控并剖析了必要性。最终摸索出一套高效的内存透露检工具的计划,其劣势在于:

  • 更精确:包含外围透露对象 widget,Layer,State;间接监控透露的本源;齐备定义内存透露
  • 更高效:自动化检测透露对象,更加短和间接的援用链
  • 业务无感知:加重开发累赘

这是业界独创的一套逻辑齐备,实用价值高,高效自动化的内存透露检测工具,堪称最强 Flutter 内存透露检测工具计划。

该计划能够笼罩咱们以后遇到所有的内存透露问题,大大晋升内存透露检测效率,为咱们业务 Flutter 化保驾护航。目前计划实现基于 Hummer 引擎,运行在 debug,profile 模式下,后续会摸索线上 release 模式检测,笼罩本地无奈复现的场景。

咱们有打算提供针对非 Hummer 引擎的接入形式,反哺社区,敬请期待。

正文完
 0