Flutter3.0 性能优化系列|一文教你完全掌握内存优化和dark虚拟机 (内存泄露)
1). dart代码都会映射c++中对应的方法2). 对象存放在 WeakPersitentHandle中3). 根据渲染原理: 通过检测渲染树节点数量来检测内存泄漏, 对比当前帧使用的EngineLayer个数,如果内存中的EngineLayer个数长时间大于使用的个数4). 通过WeakPersitentHandle找到调用链内存泄露工具: (重点分析)内存泄露具体代码总结:1. 弱引用Exp
1. 内存和虚拟机的概念
flutter从使用的语言上,可以分成3大部分,
- Framework层 由Dart编写,开发者接触到顶层,用于应用层开发
- Engine 层,由C/C++编写,主要进行图形渲染
- Embedder层,由植入层语言编写,如iOS使用Objective-C/swift,Android使用java
当我们从进程角度谈论flutter应用的内存时,指的是这个三者所有的内存的总和。
Dart虚拟机的理解:
在Flutter中,内存管理是由Dart虚拟机负责的。Dart虚拟机使用垃圾回收器来管理内存,这意味着开发者不需要手动分配和释放内存。垃圾回收器会自动识别不再使用的对象并将其回收,从而释放内存。
Flutter应用程序中的内存分为两种类型:堆内存和栈内存。堆内存用于存储对象,栈内存用于存储临时变量。在Flutter中,大多数对象都是在堆内存中创建的,而临时变量则是在栈内存中创建的。当一个对象不再被引用时,垃圾回收器会自动回收它所占用的堆内存。
Dark虚拟机:
Dart虚拟机是一个支持JIT编译和AOT编译的强类型脚本语言虚拟机,它可以运行在多种平台上,包括PC、移动设备和嵌入式设备上。Dart虚拟机内置了大量的标准库和工具,包括Dart SDK、Dart Analyzer、Dartfmt等,方便开发者进行开发和维护。Dart虚拟机支持使用各种IDE,包括Flutter、IntelliJ IDEA等,提供了强大的代码编辑和调试功能,大大提高了开发效率。
Dart虚拟机的设计架构具有良好的可扩展性和可定制性,支持诸如Dart的Native扩展等机制,可以在运行时加载和卸载模块进行动态扩展。Dart虚拟机还支持一些高级语言特性,如异步编程和生成器,使得开发者可以编写更加高效的、带有处理异步逻辑的程序。同时,Dart虚拟机还支持代码优化和预编译等特性,以提高程序的性能和运行效率。
总之,Dart虚拟机是一个具有很多优良特性的强类型脚本语言虚拟机,与Flutter等生态系统紧密结合,为移动应用开发提供了一个全面、高效的解决方案。
重点:
Dart虚拟机在初始化时,会将C++声明的某个类或者函数和某个函数和Dart中的某个类或者绑定起来,依次注入Dart运行时的全局遍历中,当Dart代码执行某一个函数时,便是指向具体的C++对象或者函数。
2. 内存监测方法:
2.1 flutter编译模式
Flutter支持Release、Profile、Debug编译3种模式。
- Release模式,使用AOT预编译模式,预编译为机器码,通过编译生成对应架构的代码,在用户设备上直接运行对应的机器码,运行速度快,执行性能好;此模式关闭了所有调试工具,只支持真机。
- Profile模式,和Release模式类似,使用AOT预编译模式,此模式最重要的作用是可以用DevTools来检测应用的性能,做性能调试分析。
- Debug模式,使用JIT(Just in time)即时编译技术,支持常用的开发调试功能hot reload,在开发调试时使用,包括支持的调试信息、服务扩展、Observatory、DevTools等调试工具,支持模拟器和真机。
2.2 开启profile模式
Android-Android Studio 开发工具来查看 APP 内存占用; 按照 Flutter 官方的性能优化指南,**Flutter-profile 模式下,使用 devTools **查看 Flutter 内存占用;
图片过载检测:切换到 Flutter Inspector,点击 Highlight Oversized Images:
2.3 、检测消耗多余内存的图片
Flutter Inspector:点击 “Highlight Oversizeded Images”,它会识别出那些解码大小超过展示大小的图片,并且系统会将其倒置,这些你就能更容易在 App 页面中找到它。
3. 优化方向
3. 1 内存泄露原理:(重点)
从渲染原理出发探究Flutter内存泄漏
3.1.1案例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),再结合源码进行分析,最后找到错误代码。
使用Flutter自带的Observatory纵然可以一个一个查看每个泄漏对象的引用关系,但是对于一个稍微复杂一点的界面而言,最终生成的layer个数是非常庞杂的,想要在Observatory所有的泄漏对象中找到有问题的代码是一项非常庞杂的任务。
为此我们将这些繁杂的定位工作都进行了可视化。
我们这里将每一帧提交到engine的所有EngineLayer进行了一个记录,并且以折线图的形式记录下来,如果上文说的内存中的layer个数异常的大于使用中的layer个数,那么就可判断前一个页面存在有内存泄漏。
Dart代码中都是通过往ui.SceneBuilder添加EngineLayer的方式去构建渲染树,那么我们只要检测c++中内存中EngineLayer的个数,对比当前帧使用的EngineLayer个数,如果内存中的EngineLayer个数长时间大于使用的个数,那么我们可以判断存在有内存泄漏
内存泄露检测原理总结:
1). dart代码都会映射c++中对应的方法
2). 对象存放在 WeakPersitentHandle中
3). 根据渲染原理: 通过检测渲染树节点数量来检测内存泄漏, 对比当前帧使用的EngineLayer个数,如果内存中的EngineLayer个数长时间大于使用的个数
4). 通过WeakPersitentHandle找到调用链
内存泄露工具: (重点分析)
内存泄露具体代码总结:
1. 弱引用Expando
2. 解决访问的私有属性Dart vm_service
3. 判断内存泄露
先通过 vm_service 获取到 Instance
,遍历里面的 fields
属性,找到 _data
字段(注意 _data
是 ObjRef 类型),用同样的办法把 _data
字段转成 Instance
类型(_data 是个数组,Obj 里面有数组的 child 信息)。
遍历 _data
字段,如果都是 null,表明我们观测的 key 对象已经被释放了。如果 item 不为 null,再次把 item 转为 Instance
对象,取它的 propertyKey
4. 手动调用GC
5.获取泄漏路径
6. 优化泄漏路径, 得到最短泄漏路径, 通过聚合和分类进行优化
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,attach 之后访问 service 提供的 http 链接即可使用该工具,内存排查功能路径为:Isolate (main) --> Allocation Profile。
工具有了,如何排查定位导致内存泄漏的问题代码呢?
对同一个功能或页面进行反复相同的进入、退出操作;
然后执行强制 GC,查看不同操作后的内存快照;
对比该功能关联的对象实例增加情况;
如果强制 GC 后实例只增不减或该回收的对象没有被回收,没有特殊的延时处理一般就可以判断相关代码有问题
3.3 图片和listview优化
重绘情况,酌情考虑优化方案(**控制 setState 粒度、**引入全局状态管理-redux/Provider、拆分 ViewModel、减少 build() 操作 等)
针对这些图片,你可以指定 cacheWidth 和 cacheHeight 为展示大小,这样可以让 flutter 引擎以指定大小解析图片,减少内存消耗。
针对 ListView item 中有 image 的情况来优化内存
ListView 不会销毁那些在屏幕可视范围之外的那些 item,如果 item 使用了高分辨率的图片,那么它将会消耗非常多的内存。
ListView 在默认情况下会在整个滑动/不滑动的过程中让子 Widget 保持活动状态,这一点是通过 AutomaticKeepAlive 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,以使被包裹的子 Widget 保持活跃。 其次,如果用户向后滚动,则不会再次重新绘制子 Widget,这一点是通过 RepaintBoundaries 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,它会让被包裹的子 Widget 仅仅绘制一次,以此获得更高的性能。 但,这样的问题在于,如果加载大量的图片,则会消耗大量的内存,最终可能使 App 崩溃。
图片方案是自研的外接纹理方案
虽然实现了一个App内一个内存缓存,并且将纹理和Flutter图片都存进去了,节省了内存空间,提高了内存使用率,但还是侵入了ImageCache源码,后续flutter engine的升级和代码维护,需要有额外的工作。
为什么flutter图片加载要采用外接纹理方案?
其实Flutter本身已具备加载图片的能力,Image组件就满足网络图片、本地图片、文件图片的加载。那为什么我们还需要实现其他图片加载方案呢?其实是因为Flutter图片组件功能上存在一些缺陷:
- 图片缓存没有持久化能力,无网环境下不支持显示图片。
- 文件图片与原生环境不共用,导致图片资源文件重复。
我们不仅实现图片方案的本地能力复用,而且还能实现视频能力的纹理外接
内存性能不足
从整个APP的视角来说,采用原生图片方案的情况下,其实我们维护了两个大的缓存池:一个是Native的图片缓存,一个是Flutter侧的图片缓存。两个缓存无法互通,这无疑是一个巨大的浪费。特别是对内存的峰值内存性能产生了非常大的压力。
调研梳理所有优化方向,确定图片压缩、更换图片组件、降低页面刷新次数三个方向
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 使用异步方式加载数据
采用异步方式优化数据加载,可以避免在程序运行时出现阻塞,使得应用程序可响应。异步方式可以优化数据请求、数据转换等操作,减少应用程序的响应时间。
综上所述,Flutter内存优化的具体实现有许多技巧和方法。开发人员可以根据实际情况综合运用各种优化策略,以提高应用程序的内存使用效率,提高用户的使用体验。
内存性能优化总结:
1. 内存泄露
2. listview优化
3. 图片优化, 图片外接纹理优化
4. 页面不可见,内存释放优化!
最后
为了能够方便大家快速学习Flutter, 这里整理了Flutter学习路线图以及《Flutter Dart 语言编程入门到精通》&《Flutter实战:第二版》帮助大家配置相关环境,学习Flutter 的基本语法以及最后的项目实际利用。
学习路线:
Dart语言是Flutter的开发语言,所以我们需要掌握Dart语言的基础知识, 由于内容过多,截图展示的目录及部分内容,完整文档领取方式扫描下方二维码即可免费获取!
《Flutter Dart 语言编程入门到精通》
第一章 Dart语言基础
- 环境准备
- 基础语法
第二章 Dart 异步编程
- Dart的事件循环
- 调度任务
- 延时任务
- Future详解
- async和await
- lsolate
第三章 异步之 Stream 详解
- 什么是Stream
- 单订阅流
- 广播流
- Stream Transformer
- 总结
第四章 Dart标准输入输出流
- 文件操作
第五章 Dart 网络编程
- TCP服务端
- TCP客户端
- UDP服务端
- UDP客户端
- HTTP服务器与请求
- WebSocket
第六章 Flutter 爬虫与服务端
- Dart爬虫开发
- Dart服务端
- 创建Flutter项目演示
- 总结
第七章 Dart 的服务端开发
- 注册登录实现
第八章 Dart 调用C语言混合编程
- 环境准备
- 测试Dart ffi接口
- 总结
第九章 LuaDardo中Dart与Lua的相互调用
- Lua C API
- 创建运行时
- Dart调Lua
- Lua调Dart
掌握了Dart语言之后,咱们就可以通过实战来掌握Flutter的知识点, 由于内容过多,截图展示的目录及部分内容,完整文档领取方式扫描下方二维码即可免费获取!
《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 进度指示器
第四章:布局类组件
- 4.1 布局类组件简介
- 4.2 布局原理与约束(constraints)
- 4.3 线性布局(Row和Column)
- 4.4 弹性布局(Flex)
- …
第五章:容器类组件
- 5.1 填充(Padding)
- 5.2 装饰容器(DecoratedBox)
- 5.3 变换(Transform)
- 5.4 容器组件(Container)
- 5.5 剪裁(Clip)
- 5.6 空间适配(FittedBox)
- 5.7 页面骨架(Scaffold)
第六章:可滚动组件
- 6.1 可滚动组件简介
- 6.2 SingleChildScrollView
- 6.3 ListView
- 6.4 滚动监听及控制
- …
第七章:功能型组件
- 7.1 导航返回拦截(WillPopScope)
- 7.2 数据共享(InheritedWidget)
- 7.3 跨组件状态共享
- 7.4 颜色和主题
- 7.5 按需rebuild(ValueListenableBuilder)
- 7.6 异步UI更新(FutureBuilder、StreamBuilder)
- 7.7 对话框详解
第八章:事件处理与通知
- 8.1 原始指针事件处理
- 8.2 手势识别
- 8.3 Flutter事件机制
- 8.4 手势原理与手势冲突
- 8.5 事件总线
- 8.6 通知 Notification
第九章:动画
- 9.1 Flutter动画简介
- 9.2 动画基本结构及状态监听
- 9.3 自定义路由切换动画
- 9.4 Hero动画
- 9.5 交织动画
- 9.6 动画切换组件(AnimatedSwitcher)
- 9.7 动画过渡组件
第十章:自定义组件
- 10.1 自定义组件方法简介
- 10.2 组合现有组件
- 10.3 组合实例:TurnBox
- 10.4 CustomPaint 与 Canvas
- 10.5 自绘实例:圆形背景渐变进度条
- 10.6 自绘组件:CustomCheckbox
- 10.7 自绘组件: DoneWidget
- 10.8 水印实例: 文本绘制与离屏渲染
第十一章:文件操作与网络请求
- 11.1 文件操作
- 11.2 通过HttpClient发起HTTP请求
- 11.3 Http请求库-dio
- 11.4 实例:Http分块下载
- …
第十二章:Flutter扩展
- 12.1 包和插件
- 12.2 Flutter Web
第十三章:国际化
- 13.1 让App支持多语言
- 13.2 实现Localizations
- 13.3 使用Intl包
- 13.4 国际化常见问题
第十四章:Flutter核心原理
- 14.1 Flutter UI 框架(Framework)
- 14.2 Element、BuildContext和RenderObject
- 14.3 Flutter启动流程和渲染管线
- 14.4 布局(Layout)过程
- 14.5 绘制(一)绘制原理及Layer
- …
第十五章:一个完整的Flutter应用
- 15.1 Github客户端示例
- 15.2 Flutter APP代码结构
- 15.3 Model类定义
- 15.4 全局变量及共享状态
- 15.5 网络请求封装
- 15.6 APP入口及主页
- 15.7 登录页
- 15.8 多语言和多主题
由于内容过多,截图展示的目录及部分内容,完整文档领取方式扫描下方二维码即可免费获取!
更多推荐
所有评论(0)