Flutter 使用 dart 语言作为其开发语言和运行环境. dart 的 runtime 是一直存在的, 但是在 debug 和 release 模式下有一些区别.
在 debug 模式下, dart 大部分组件都放在设备上, 例如 runtime,JIT(Android),interpreter(iOS),debug 和 profile services.
在 release 模式下, 只剩下 runtime, 而这也是 Flutter App 能够运行起来的最基本组件.
在 runtime 中, 存在一个在初始化对象时为其分配内存, 对象不再被使用的时候回收内存的组件, 即 GC. 在 Flutter 中存在很多对象. 以 Stateless Widget 为例, 其在 State 发生变化或者 Widget 不可见的时候不断地发生重建和销毁(注意, 此处是指 Widget 树中的 Widget, 对于 Element 树和 RenderObject 树来说, element 和 renderObject 是可变的, 而且其初始化生成需要消耗很多资源. 因此在大多数情况下他们是会被回收利用的). 这些 Widget 的生命周期都很短, 对于一个 UI 比较复杂的 App 来说, 可能会有数千个 Widget 需要被经常回收创建.
所以有些开发者可能会采取一些措施来避免太过频繁的 GC. 比如为了保持一个引用的 Widget 对象不会被回收, 将其放在 state 中(这样并不是说真的不会被回收, 只是创建回收的频率被降低了, 因为 state 是属于 element 的, 而 element 的生命周期是比较长的).
这么做是没有必要的, 首先 Widget 是一个很轻量级的对象, 它的创建和回收并不会占用很多资源, 真正占用资源的是 Element 和 RenderObject. 其次 dart 的 GC 机制能够快速有效的进行对象回收, 不用担心 Widget 创建过多导致 OOM 出现.
关于 Widget,Element,RenderObject 的更多关系请参看这篇文章 Flutter 中的层级蛋糕 https://www.jianshu.com/p/8e714a204898 .
Dart GC
和 Jvm 类似, dart 中的 GC 是分代的, 一个是年轻代, 一个是老年代. 如果熟悉 Jvm 内存机制的童鞋可以快速略过这一部分, 直接看最下面的结论.
1. schedule
首先介绍下 dart 中的调度机制: 为了最小化 GC 对 App 和 UI 的性能影响 (因为 dart 的 GC 有一种类似于 JVM 中 stop the world 的机制, 导致 App 对事件无响应, UI 无法刷新),GC 通过与 Flutter 的 engine 建立联系, 在 Flutter App 处于空闲, 无用户交互, 或者在后台的情况下, engine 通知 GC 进行回收工作. 这样就不会对 App 和 UI 产生影响了. 同时 GC 还会使用滑动压缩(类似于 JVM 中标记整理中的整理) 的方法来减少内存碎片的数量, 从而减少内存开销.
2. 年轻代
dart 内存中的年轻代和 JVM 中的年轻代很相似. 年轻代上存放的对象是那些生命周期较短, 需要经常创建回收的对象, 例如 stateless widget. 年轻代的 GC 比老年代的频繁很多, 速度也比老年代快. 配合上 schedule 机制, 我们在 App 运行的时候几乎感觉不到 GC 造成的卡顿. 在本质上, 对象占据了内存中的连续空间. 随着对象的创建, 它们被分配给下一块可用的内存空间, 直到所有的内存都被占满了, 然后进行 GC.dart 使用指针碰撞的方式来给这些对象分配空间(之所以没有空闲列表的方法是因为 dart 在 GC 之后都会采用滑动压缩的方式来把内存碎片清除掉). 和 JVM 类似, dart 的年轻代也分成两个部分, 在任何时候, 只会有一部分被使用.
如图, 在进行 GC 的时候, 首先遍历 from 区域中的对象, 判断其是否可以被回收(采用可达性分析方法), 遍历完成之后将不会被回收的对象复制到 to 区域中, 然后 from 区域中的对象全部被回收掉. 最后原来的 to 区域就变成 from 区域. 吐个槽, 可能这种回收方式还会修改, 改成 JVM 中 8:1:1 的方式, 因为每次都只能使用一半的内存, 实在是太浪费内存了. 再补个图供参考
3. 老年代
当对象经历过一定次数的 GC 仍然存在, 或者其生命周期较长(个人猜测类似于 element 和 RenderObject 这种需要多次复用, 可变且创建比较耗费性能), 将其放入老年代区域中. 老年代采用标记整理的方法来回收对象.
在标记的时候, 该线程中内存区域是处于不可修改的状态, 类似于 JVM 中 stop the world, 所以这个时候可能会导致 ANR(只是类似于 ANR 的表现, 其产生原因还是不一样的), 但是由于 dart 优秀的 schedule 机制和老年代 GC 频率很低的原因, 基本上不会出现这个问题.
需要注意的是, 如果 App 不支持弱年代假设(即大多数对象的生命期都很短; 从年老对象到年轻对象的引用非常少), 上面的分代设计就不那么有效了, 但是考虑到 Flutter 中的 Widget,Element,RenderObject 关系, 我们不需要担心这个问题.
4. isolate
与 JVM 内存模型不同的是, dart 中每个 isolate 都有自己的内存空间, 其各自的 GC 不会影响到其他 isolate 的. 所以我们可以通过把部分占用内存空间较大且生命周期较短的对象方法其他 isolate 中, 这样即使另外一个 isolate GC 了, 并不会对我们显示 UI 的 isolate 造成影响.
与 isolate 相关的知识请参看小德大佬的文章:
深入了解 Flutter 的 isolate(1) ---- 事件循环 (event loop) 及代码运行顺序
深入了解 Flutter 的 isolate(2) --- 创建自己的 isolate
深入了解 Flutter 的 isolate(3) --- Flutter 的 thread model(线程模型)
深入了解 Flutter 的 isolate(4) --- 使用 Compute 写 isolates
总结
dart | java | |
---|---|---|
判断对象可被回收算法 | 可达性分析 | 可达性分析 |
年轻代 GC 算法 | 复制 1:1 | 复制 8:1:1 |
老年代 GC 算法 | 标记整理 | 标记整理 |
是否发生 stop the world | 是 | 是 |
引用文章
Flutter: Don't Fear the Garbage Collector
Android Flutter 实践内存初探 https://www.yuque.com/xytech/flutter/avmyht
来源: https://juejin.im/post/5c7e0c255188250a432388d8