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、网络等操作。