美文网首页
1.遥遥领先 Flutter3.0 性能优化系列|一文教你完全掌

1.遥遥领先 Flutter3.0 性能优化系列|一文教你完全掌

作者: 鹏城十八少 | 来源:发表于2024-03-11 14:34 被阅读0次

    目录;

    1. 内存和虚拟机的概念

    2. 内存检测方案

    内存泄露检测原理, 工具的代码具体实现, 具体的内存泄露案例

    3. 优化手段

    4. 优化项的总结

    内存泄露工具.png

    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种模式。

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

    2.2 开启profile模式

    Android-Android Studio 开发工具来查看 APP 内存占用; 按照 Flutter 官方的性能优化指南,**Flutter-profile 模式下,使用 devTools **查看 Flutter 内存占用;

    优化工具1.jpg

    图片过载检测:切换到 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的子树都无法释放

    案例图解.jpg

    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个数长时间大于使用的个数,那么我们可以判断存在有内存泄漏

    渲染_内存泄露.jpg
    内存泄露检测原理总结:

    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层可以稳定感知到页面栈的变化。

    性能优化layout.jpg

    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优化

    1. 图片优化, 图片外接纹理优化

    4. 页面不可见,内存释放优化!

    相关文章

      网友评论

          本文标题:1.遥遥领先 Flutter3.0 性能优化系列|一文教你完全掌

          本文链接:https://www.haomeiwen.com/subject/xwpmzdtx.html