Flutter3.0 性能优化系列 | 一文教你完全掌握内存优化和Dart虚拟机

1. 内存和虚拟机的概念

Flutter从使用的语言上,可以分成3大部分:

  • Framework层 由Dart编写,开发者接触到顶层,用于应用层开发
  • Engine 层,由C/C++编写,主要进行图形渲染
  • Embedder层,由植入层语言编写,如iOS使用Objective-C/swift,Android使用java

当我们从进程角度谈论Flutter应用的内存时,指的是这三者所有的内存的总和。

Dart虚拟机的理解

在Flutter中,内存管理是由Dart虚拟机负责的。Dart虚拟机使用垃圾回收器来管理内存,这意味着开发者不需要手动分配和释放内存。垃圾回收器会自动识别不再使用的对象并将其回收,从而释放内存。

Flutter应用程序中的内存分为两种类型:堆内存和栈内存。堆内存用于存储对象,栈内存用于存储临时变量。在Flutter中,大多数对象都是在堆内存中创建的,而临时变量则是在栈内存中创建的。当一个对象不再被引用时,垃圾回收器会自动回收它所占用的堆内存。

Dart虚拟机

Dart虚拟机是一个支持JIT编译和AOT编译的强类型脚本语言虚拟机,它可以运行在多种平台上,包括PC、移动设备和嵌入式设备上。Dart虚拟机内置了大量的标准库和工具,包括Dart SDK、Dart Analyzer、Dartfmt等,方便开发者进行开发和维护。

Dart虚拟机的设计架构具有良好的可扩展性和可定制性,支持诸如Dart的Native扩展等机制,可以在运行时加载和卸载模块进行动态扩展。Dart虚拟机还支持一些高级语言特性,如异步编程和生成器,使得开发者可以编写更加高效的、带有处理异步逻辑的程序。同时,Dart虚拟机还支持代码优化和预编译等特性,以提高程序的性能和运行效率。

重点:
Dart虚拟机在初始化时,会将C++声明的某个类或者函数和Dart中的某个类或者绑定起来,依次注入Dart运行时的全局遍历中,当Dart代码执行某一个函数时,便是指向具体的C++对象或者函数。

2. 内存监测方法

2.1 Flutter编译模式

Flutter支持Release、Profile、Debug编译3种模式:

  1. Release模式:使用AOT预编译模式,预编译为机器码,通过编译生成对应架构的代码,在用户设备上直接运行对应的机器码,运行速度快,执行性能好;此模式关闭了所有调试工具,只支持真机。
  2. Profile模式:和Release模式类似,使用AOT预编译模式,此模式最重要的作用是可以用DevTools来检测应用的性能,做性能调试分析。
  3. Debug模式:使用JIT(Just in time)即时编译技术,支持常用的开发调试功能hot reload,在开发调试时使用,包括支持的调试信息、服务扩展、Observatory、DevTools等调试工具,支持模拟器和真机。

2.2 开启Profile模式

按照Flutter官方的性能优化指南,Flutter-profile模式下,使用DevTools查看Flutter内存占用。

检测消耗多余内存的图片

Flutter Inspector:点击"Highlight Oversized Images",它会识别出那些解码大小超过展示大小的图片,并且系统会将其倒置,这些你就能更容易在App页面中找到它。

3. 优化方向

3.1 内存泄露原理

从渲染原理出发探究Flutter内存泄漏

案例1:
例如,存在两个A,B界面,A界面通过Navigator.push的方式添加B界面,B界面通过Navigator.pop回退到A。如果B界面因为某些写法的缘故导致B的渲染树虽然被从主渲染树解开后依然无法被释放,这会导致整个原来B的子树都无法释放。

3.1.2 内存泄工具设计原理

Flutter内存泄漏检测工具的设计思路是:借鉴Android LeakCanary,用WeakPersitentHandle对象,通过检测渲染树节点来检测内存泄漏。

对比界面进入前后的对象,寻找出未被释放的对象,进而查看未释放的引用关系(Retaining path或Inbound references),再结合源码进行分析,最后找到错误代码。

内存泄露检测原理总结:

  1. dart代码都会映射c++中对应的方法
  2. 对象存放在WeakPersitentHandle中
  3. 根据渲染原理:通过检测渲染树节点数量来检测内存泄漏,对比当前帧使用的EngineLayer个数,如果内存中的EngineLayer个数长时间大于使用的个数
  4. 通过WeakPersitentHandle找到调用链

3.2 内存泄漏的常见场景

案例:
目前发现异步执行的代码的场景(Feature, async/await,methodChan)长期持有传入的BuildContext,导致element被移除后,依然长期存在,最终导致以及关联的widget,state发生泄漏。

3.2.1. 监听反注册缺失

排查内存泄漏的过程中,我们发现图片内存大幅度超出了图片缓存自身size限制的增长,并且不会被GC回收,经过排查发现我们封装的一个底层图片处理类,注册了图片事件流监听后,并没有在适当的时机做反注册处理。

3.2.2. 长列表直接构建列表项

通过对列表数据遍历的方式,一次生成所有数据对应的widget列表,直接塞进Column里展示给用户,当加载了几页数据之后,数据量稍大就会轻易导致OOM或导致严重卡顿。

3.2.3. 延时、持续执行的闭包引用

Flutter提供的延时和持续执行的对象有Animation、Timer、Future等,在结束执行之前,回调函数引用到的相关对象都会被强引用保留在内存中。

3.3 内存泄漏的操作步骤和实战

通过Observatory分析内存泄漏

Android Studio或VS Code插件帮我们包装了相关内存工具,这些工具都基于debug模式下Dart VM service暴露的接口开发的,Dart VM service自身也带有协助排查内存问题的工具 - Dart VM Observatory。

工具有了,如何排查定位导致内存泄漏的问题代码呢?

对同一个功能或页面进行反复相同的进入、退出操作;
然后执行强制GC,查看不同操作后的内存快照;
对比该功能关联的对象实例增加情况;
如果强制GC后实例只增不减或该回收的对象没有被回收,没有特殊的延时处理一般就可以判断相关代码有问题。

3.3 图片和ListView优化

重绘情况,酌情考虑优化方案(控制setState粒度、引入全局状态管理-redux/Provider、拆分ViewModel、减少build()操作等)。

针对这些图片,你可以指定cacheWidth和cacheHeight为展示大小,这样可以让Flutter引擎以指定大小解析图片,减少内存消耗。

针对ListView item中有image的情况来优化内存

ListView不会销毁那些在屏幕可视范围之外的那些item,如果item使用了高分辨率的图片,那么它将会消耗非常多的内存。

3.4 多页面内存优化

这个优化策略是真真被逼出来的。在对线上数据分析以后,我们发现Flutter页面栈有一个非常有意思的特点:多页面栈情况下,底层的页面不会被释放。即便是在内存非常紧张的情况下,也不会执行回收。这样就会导致一个问题:随着页面的增多,内存消耗会线性增长。这里占比最高的就是图片资源的占比了。

我们发现Flutter的layer层可以稳定感知到页面栈的变化。

4. 写代码时候,内存总结

Flutter内存优化是一个非常复杂的问题,其中涉及多个方面的优化策略。下面将从以下几个方面对Flutter的内存优化进行具体实现的总结。

一、减少Widget的创建和销毁

Widget的创建和销毁是Flutter中内存占用最大和最频繁的操作之一,在开发过程中,应该尽量减少Widget的创建和销毁。

1.1 重用现有的Widget

在同一个页面内,如果多个Widget具有相同的样式和行为,则可以共用同一个widget,而不是每个Widget都创建自己的。

1.2 使用Key避免重复构建

Flutter中相同类型的Widget,如果没有设置Key属性,则每次更新时都会创建新的Widget,即使两个Widget的props完全一致。相反,如果为Widget设置Key属性,则当Widget更新时,会尝试在它们之间保持一个一致的关系,从而减少重新构建它们所需要的树的大小。

1.3 避免状态管理器泄露

在Flutter中,每个控件对应一个状态,如果状态管理不当,可能会导致内存泄漏。因此,在组件销毁时必须释放所占用的内存。在实现中可以使用StatefulWidget和StatelessWidget等控件,这些控件会自动处理状态管理的问题。

二、优化图片资源

在Flutter中,图片资源可能会占用大量的内存,如果没有得到合适的管理和优化,很容易导致内存溢出的问题。

2.1 去掉不必要的图片

去掉那些不必要的图片,或者压缩那些需要的图片,以减少内存占用。

2.2 避免图片重复加载

重复加载的图片会占用更多的内存,因此应该尽量避免这种情况的发生。在Flutter中可以使用MemoryImage对图片进行缓存,提高图片的复用率。

三、避免资源泄漏

资源泄漏是内存占用过多的常见问题之一,在Flutter中也是如此。应该在开发过程中注意避免资源泄漏。

3.1 及时释放内存

当不再使用某个已分配的内存时,应该及时释放它,避免内存泄漏等问题。

3.2 避免"闭包"泄漏

在Flutter中,如果一个Widget包含了另一个Widget,则可能会发生"闭包",即某个Widget的生命周期不正确地延长。为了避免这种情况的发生,可以使用StatelessWidget等控件,不包含状态变量,避免"闭包"泄漏。

四、合理管理内存

在Flutter中,应用程序使用的内存受到操作系统、硬件性能等多个因素的影响。为了合理地管理内存,需要进行特定的优化,以确保应用程序不会消耗过多的内存。

4.1 使用Navigator管理页面历史

在Flutter中,能够使用Navigator管理页面历史,并对页面进行统一的管理。使用Navigator可以对页面进行释放和回收,避免页面占用过多内存。

4.2 使用异步方式加载数据

采用异步方式优化数据加载,可以避免在程序运行时出现阻塞,使得应用程序可响应。异步方式可以优化数据请求、数据转换等操作,减少应用程序的响应时间。

内存性能优化总结:

  1. 内存泄露
  2. ListView优化
  3. 图片优化,图片外接纹理优化
  4. 页面不可见,内存释放优化!

5. 开发工具推荐

在进行Flutter应用开发时,合理使用工具可以极大提升开发效率。对于iOS开发者来说,AppUploader是一款不错的辅助工具,它可以帮助开发者快速完成应用打包、上传到App Store等流程,特别适合需要频繁打包测试的团队使用。

AppUploader的主要特点包括:

  • 简化iOS应用打包流程
  • 支持批量处理多个应用
  • 提供直观的界面操作
  • 自动处理证书和描述文件

对于Flutter开发者来说,结合AppUploader可以更高效地管理iOS端的发布流程,特别是在进行性能优化后的版本验证时,能够快速打包测试。

6. 学习资源推荐

为了帮助大家更好地掌握Flutter开发,这里推荐一些学习资源:

《Flutter Dart 语言编程入门到精通》

第一章 Dart语言基础
  • 环境准备
  • 基础语法
第二章 Dart 异步编程
  • Dart的事件循环
  • 调度任务
  • 延时任务
  • Future详解
  • async和await
  • Isolate
第三章 异步之 Stream 详解
  • 什么是Stream
  • 单订阅流
  • 广播流
  • Stream Transformer
  • 总结
第四章 Dart标准输入输出流
  • 文件操作
第五章 Dart 网络编程
  • TCP服务端
  • TCP客户端
  • UDP服务端
  • UDP客户端
  • HTTP服务器与请求
  • WebSocket

《Flutter实战:第二版》

第一章:起步
  • 1.1 移动开发技术简介
  • 1.2 初始Flutter
  • 1.3 搭建Flutter开发环境
  • 1.4 Dart语言简介
第二章:第一个Flutter应用
  • 2.1 计数器应用实例
  • 2.2 Widget简介
  • 2.3 状态管理
  • 2.4路由管理
  • 2.5包管理
  • 2.6 资源管理
  • 2.7 调试Flutter应用
  • 2.8 Flutter异常捕获
第三章:基础组件
  • 3.1 文本及样式
  • 3.2 按钮
  • 3.3 图片及ICON
  • 3.4 单选开关和复选框
  • 3.5 输入框及表单
  • 3.6 进度指示器

通过系统学习这些内容,开发者可以全面掌握Flutter开发技能,并能够有效地进行应用性能优化。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐