Flutter Dart垃圾回收


Flutter使用Dart作为开发语言以及运行时,Dart 运行时存在于Debug和Release模式,不过两个模式中的运行时有很大的区别。

Debug模式下,Dart运行时,JIT编译器/解释器(Android是JIT,iOS是解释器),Debug和Profile工具都会加载到设备上。而Release模式下,JIT编译器/解释器和Debug服务被移除,只包括运行时。

Dart运行时包含垃圾回收器,在对象创建和销毁时分配和释放内存。Flutter在运行中,会创建大量的对象,比如,伴随着在界面展示,不可见,app 状态变化等,会创建很多Staleless Widgets,其中很多生命周期都很短。一个比较复杂的UI界面,widgets数量能上千。

那么Flutter 开发者是否需要关心垃圾回收?Flutter在高频次创建和销毁对象的情况下,开发者是否需要采取措施来限制这种行为?很多人会对不会变的widgets创建引用,保存在state中,来避免重建和销毁。其实这些是没有必要的,因为Dart垃圾回收器基于分代架构,且针对对象的快速创建和销毁做了优化,大多数情况下,让引擎自己管理所有widgets创建和销毁即可。

Dart垃圾回收器


Dart的垃圾回收器由两部分组成:新生代scavenger和并发标记清除回收器。

调度

为了降低垃圾回收对UI性能的影响,垃圾回收器提供了hooks给flutter engine,当没有用户交互或app处于闲时状态时,flutter engine会通知垃圾回收器来进行收集,而不会影响想能。

当处于闲时状态时,垃圾回收器也能进行内存碎片整理,减少内存碎片。

年轻代Scavenger

该阶段主要用来清理生命周期短的临时对象,如stateless widgets,会比次代标记/清除阶段更快,极大减小app运行过程中可能带来的卡顿。

对象创建时会在一片连续的内存空间进行分配,每次创建对象,都会再这片空间区域查找是否有可用的空间,当可用空间被占满,Dart会使用bump pointer(指针碰撞)的方式来快速分配新的空间。

新的空间分为两部分,称为半空间,任何时候,只有一半是处于激活的,另一半则处于非激活状态。新创建的对象会在激活的那部分空间进行分配,当激活部分被占满,引用不可释放的对象会从激活空间移到非激活空间,非激活空间将变为激活空间,以此反复。

为了确定当前的对象是否可释放,收集器会从根对象,如栈变量,来检测是否是否还有引用,引用的对象会被移动到非激活空间,最后,剩下的对象就是可释放对象,在后续的垃圾回收时,引用对象会直接覆盖之前的可释放对象占用的空间。更多细节,参看Cheney’s 算法

并行标记和并发清除

当对象到达一定的生命周期,他们会被移入新的内存空间(老年代),由次代收集器(标记/清除)进行内存管理,收集器同样分两部分:首先会遍历对象图,标记还在使用的对象。遍历完成后,未被标记的对象会被回收。

注意,这种形式的垃圾回收会在标记阶段阻塞UI线程,且不能进行内存修改操作。不过,这个阶段发生的频率很低,因为生命周期短的对象都被年轻代scavenger处理了。

当然,如果开发的app不满足弱分代假说(即大多数对象会在年轻时死亡,生命周期短)的情况,那么该形式的垃圾回收会更容易发生。

Heap Map

Observatory下可以通过heap map来观察某一时刻特定Isolate老年代的内存分配,如下图,不同的颜色代表不同的内存块,白色代表空闲空间,如果我们看到很多小的白色块,说明产生了内存碎片,可能考虑有内存泄漏了。

Isolates


Dart的每个Isolate有自己的堆空间,每个Isolate也都运行在单独的线程中,垃圾回收时,互不影响性能,所以Isolate可以避免阻塞UI,进行CPU密集型、IO、网络等操作。

附录


  1. https://mrale.ph/dartvm/
  2. https://medium.com/flutter/flutter-dont-fear-the-garbage-collector-d69b3ff1ca30
  3. https://dart-lang.github.io/observatory/heap-map.html
  4. https://dart-lang.github.io/observatory/glossary.html?spm=ata.13261165.0.0.1f0058e56w9Oaq#memory-leak