Flutter 开发App优化之旅

最近做了几年的flutter开发,跟随flutter版本从1.*到现在的3.*的项目迭代中,也逐渐开始着手项目的性能优化体验,一些基础的flutter垃圾回收机制(GC)的原理这里也不再过多的解释,想必相关的开发人员也早有些耳闻,不是很清楚的也可以去网络上查看大神的一些原理性文章。这里就不过多赘述。

开发检测工具我单独出了一篇文章flutter 优化检测工具icon-default.png?t=N7T8https://blog.csdn.net/BUG_delete/article/details/129295442,有需要可以去查阅参考。

本篇文章随着我的理解加深和新的技术学习,会不断的升级补充新内容。有喜欢flutter的攻城狮们可以点赞收藏本文章,以供之后及时的查看新的内容更新。

下面,我稍微总结一些自己开发flutter的项目所遇到的优化方式和方法。以供有心之人共勉。

一、内存优化

  1. 超出作用范围及时释放。
    1. 注意listener、provider等一系列对象的关闭和移除操作。
  2. 减少单例模型的创建和使用
    1. 不一定是越少越好,最主要的原则是:在退出它可使用的最小范围之后能够及时的销毁。
    2. provider的value形式也是单例。
  3. 避免异步使用context
    1. ​​​​​​​​​​​​​​
  4. 图片的加载优化

    1. 我们只用图片的时候flutter的相关图片加载构造器里面都会有width、height的参数,尽量在使用图片的地方都加上参数的设置。flutter的内部会将width、height作为缓存cache这个图片的时候的大小参数,会将图片repaintsize之后作为缓存这个图片,这就避免大图片(使用的其实不是很大的尺寸)给设备内存造成的不必要的压力问题。

  5. 及时清理(GC回收机制并没有RAC机制那么流畅,回收操作感觉并没有实时在做,就需要我们在不使用的时候及时移除占用)
    1. 图片清理:PaintingBinding.instance.imageCache.clear();
    2. webview清理缓存:_webViewController?.clearCache();注意这里的清理会清理项目中所有webview的local存储,并非只是当前webview所加载的页面
    3. Lottie清理:sharedLottieCache.clear(); (需引入路径:import 'package:lottie/src/providers/lottie_provider.dart';可能目前1.4.3版本比较奇怪引入lottie总文件路径无效)

二、启动速度优化

  1. 避免启动初始化过多耗时任务(分块加载初始化)
    1. eg:网络初始化可以放在闪屏页面,和用户强关联的模块、sdk可以放在登录之后操作。
  2. 页面启动耗时避免initState方法里面做。
    1. 任务放在状态管理机制里面处理,页面启动“友好交互”原则(加载动画、或先展示旧数据等新数据处理完成之后再刷新)
  3. 页面build里面有耗时操作
  4. 我们应该尽量避免在 build() 中执行耗时操作,因为 build() 会被频繁地调用,尤其是当 Widget 重建的时候。

    此外,我们不要在代码中进行阻塞式操作,可以将文件读取、数据库操作、网络请求等通过 Future 来转换成异步方式来完成。

    最后,对于 CPU 计算频繁的操作,例如图片压缩,可以使用 isolate 来充分利用多核心 CPU。

    isolate 作为 Flutter 中的多线程实现方式,之所以被称之为 isolate(隔离),是因为每一个 isolate 都有一份单独的内存。flutter为我们提供了compute方法来便捷使用isolate,将耗时方法交给isolate,将不影响主线程的性能。

    Flutter 会运行一个事件循环,它会从事件队列中取得最旧的事件,处理它,然后再返回下一个事件进行处理,依此类推,直到事件队列清空为止。每当动作中断时,线程就会等待下一个事件

    实质上,不仅仅是 isolate,所有的高级 API 都能够应用于异步编程,例如 Futures、Streams、async 和 await,它们全部都是构建在这个简单的事件循环之上。

    而,async 和 await 实际上只是使用 futures 和 streams 的替代语法,它将代码编写形式从异步变为同步,主要用来帮助你编写更清晰、简洁的代码。

    此外,async 和 await 也能使用 try on catch finally 来进行异常处理,这能够帮助你处理一些数据解析方面的异常。

  5. Flutter 引擎预加载(引用知乎
    1. ​​​​​​​​​​​​​​使用它可以达到页面秒开的一个效果,具体实现为:

      在 HIFlutterCacheManager 类中定义一个 preLoad 方法,使用 Looper.myQueue().addIdleHandler 添加一个 idelHandler,当 CPU 空闲时会回调 queueIdle 方法,在这个方法里,你就可以去初始化 FlutterEngine,并把它缓存到集合中。

      预加载完成之后,你就可以通过 HIFlutterCacheManager 类的 getCachedFlutterEngine 方法从集合中获取到缓存好的引擎。

  6. Dart VM 预热
    1. ​​​​​​​​​​​​​​对于 Native + Flutter 的混合场景,如果不想使用引擎预加载的方式,那么要提升 Flutter 的启动速度也可以通 过Dart VM 预热来完成,这种方式会提升一定的 Flutter 引擎加载速度,但整体对启动速度的提升没有预加载引擎提升的那么多。

      无论是引擎预加载还是 Dart VM 预热都是有一定的内存成本的,如果 App 内存压力不大,并且预判用户接下来会访问 Flutter 业务,那么使用这个优化就能带来很好的价值;反之,则可能造成资源浪费,意义不大。

  7. ​​​​​​​7、让 Flutter 中重建组件的个数尽量少

    1. 在实际开发过程中,如果将整个页面写在一个单独的 StatefulWidget 中,那么每次状态更新时都会导致很多不必要的 UI 重建。因此, 我们要学会拆解组件,使用良好设计模式和状态管理方案,当需要更新状态时将影响范围降到最小。

  8. 8、Flutter实现的一些效果背后可能会使用 saveLayer() 这个代价很大的方法​​​​​​​

  9. ​​​​​​​如下这几个组件,底层都会触发 saveLayer() 的调用,同样也都会导致性能的损耗:ShaderMask,ColorFilter,Chip(当 disabledColorAlpha != 0xff 的时候,会调用 saveLayer())、Text(如果有 overflowShader,可能调用 saveLayer() )。

  10. 9、官方也给了我们一些非常需要注意的优化点

    由于 Opacity 会使用屏幕外缓冲区直接使目标组件中不透明,因此能不用 Opacity Widget,就尽量不要用。有关将透明度直接应用于图像的示例,请参见 Transparent image,比使用 Opacity widget 更快,性能更好。

    要在图像中实现淡入淡出,请考虑使用 FadeInImage 小部件,该小部件使用 GPU 的片段着色器应用渐变不透明度。

    很多场景下,我们确实没必要直接使用 Opacity 改变透明度,如要作用于一个图片的时候可以直接使用透明的图片,或者直接使用 Container:Container(color: Color.fromRGBO(255, 0, 0, 0.5))

    Clipping 不会调用 saveLayer()(除非明确使用 Clip.antiAliasWithSaveLayer),因此这些操作没有 Opacity 那么耗时,但仍然很耗时,所以请谨慎使用。

  11. 10、代码中尽量不要使用microtask(Future.microtask,microtask 内部队列主要是由 Dart 内部产生,而 Stream 中的执行异步的模式就是 scheduleMicrotask 了)来执行任务,因为microtask的优先级高于even queue(点击、滑动、IO、绘制事件 等事件),

三、布局加载优化

  1. 避免整体刷新setState,(除非Widget页面元素较少,或者确实需要很多元素更新)
    1. 可以使用dart的StreamBuilder、ValueListenableBuilder、GlobalKey等操作局部刷新。
    2. 绘制完成的图表CustomPainter的组件,滑动期间避免重绘问题。(RepaintBoundary)或者灵活处理shouldRepaint方法​​​​​​​
      @override
        bool shouldRepaint(CustomPainter oldDelegate) {
          return oldDelegate != this;
        }
  2. 元素最小化原则
    1. 避免清一色使用Container,适当的使用SizeBox,DecoratedBox,Padding来减少组件的臃肿性。
    2. 使用nil来代替不必要的占位元素(SizeBox或者Container),你需要明白 nil 仅仅是一个基础的 Widget 元素 ,它的构建成本几乎没有。一般我们在三目运算符的时候会用的比较多。
  3. 尽可能地使用 const,抑制 widget 的重建
    1. ​​​​​​​​​​​​​​如果某一个实例已经用 const 定义好了,那么其它地方再次使用 const 定义时,则会直接从常量池里取,这样便能够节省 RAM。针对一些长期不修改的组件更应该使用。
    2. const不仅适用于字符串、数字的常量定义也适用于widget的构建
  4. build() 方法中的嵌套漩涡(这个本身没有问题)存在的问题:
    1. 代码可读性差:画界面时需要一个 Widget 嵌套一个 Widget,但如果 Widget 嵌套太深,就会导致代码的可读性变差,也不利于后期的维护和扩展。
    2. 复用难:由于所有的代码都在一个 build(),会导致无法将公共的 UI 代码复用到其它的页面或模块。
    3. 影响性能:我们在 State 上调用 setState() 时,所有 build() 中的 Widget 都将被重建,因此 build() 中返回的 Widget 树越大,那么需要重建的 Widget 就越多,也就会对性能越不利。
    4. 所以,你需要 控制 build 方法耗时,将 Widget 拆小,避免直接返回一个巨大的 Widget,这样 Widget 会享有更细粒度的重建和复用。

    5. 实际上可以将Build函数视为仅渲染UI的方法,实际情况也理所应当这样做。

  5. 合理使用StatelessWidget和StatefulWidget
    1. ​​​​​​​​​​​​​​StatelessWidget更加轻量
  6. 避免使用List.forEach进行大批量的处理
    1. ​​​​​​​​​​​​​​实际上Future是个很好的东西,比如Future.forEach同样可以遍历数组但在执行的过程中,对主线程带来的影响确是有差异的。
  7. 非必需,列表形态的可以使用懒加载(按需加载):builder的方法来加载。
    1. eg:ListView.builder()、GridView.builder(),PageView.builder()
  8. 多变图层与不变图层分离
    1. 如Gif,动画。这时我们就需要RepaintBoundary,不过独立图层合成也是有消耗,这块需实测把握。这会导致页面同一图层重新Paint。此时可以用RepaintBoundary包裹该多变的Gif组件,让其处在单独的图层,待最终再一块图层合成上屏。页面滑动将不会导致动画重绘。还可以用该widget包住我们的图片,做图片缓存,ListView里面就使用了RepaintBoundary来包住item,缓存item。​​​​​​​
      image.png

9.复用Element
        element tree是flutter三棵树之一,我个人把他当作渲染树的manager,widget内部有个canUpdate方法。

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

        在setState方法调用后,页面会刷新,widget tree会重建,element内部的diff算法会根据对应widget去比较旧widget(oldWidget)和新widget(newWidget),如果他们的runtimeType(类型)和key相同,则会复用该element节点,不会重建,这将节约很多性能。因此我们在写widget tree时,尽量减少widget类型的改变和整体结构的改变,比如使用Visibility控件替换if/else;对于key,在一个多节点的列表中,请给每个节点一个唯一key,这样可以做到复用element。
 

四、关键指标(页面异常率,页面帧率,页面加载时长等)

  1. 列表优化
    1. ​​​​​​​​​​​​​​对于长列表,记得在 ListView 中使用 itemExtent。​​​​​​​​​​​​​​​​​​​​​​​​​​​​有时候当我们有一个很长的列表,想要用滚动条来大跳时,使用 itemExtent 就很重要了,它会帮助 Flutter 去计算 ListView 的滚动位置而不是计算每一个 Widget 的高度,与此同时,它能够使滚动动画有更好的性能
    2. 减少可折叠 ListView 的构建时间。 针对于可折叠的 ListView,未展开状态时,设置其 itemCount 为 0,这样 item 只会在展开状态下才进行构建,以减少页面第一次的打开构建时间
  2. 尽量不要为 Widget 设置半透明效果
    1. ​​​​​​​​​​​​​​考虑用图片的形式代替,这样被遮挡的部分 Widget 区域就不需要绘制了。
  3. ​​​​​​​优化光栅线程
  4. 用 key 加速 Flutter 的性能优化光栅线程
  5. 低端设备特殊适配
    1. 降级CustomScrollView,ListView等预渲染区域为合理值

      默认情况下,CustomScrollView除了渲染屏幕内的内容,还会渲染上下各250区域的组件内容,例如当前屏幕可显示4个组件,实际仍有上下共4个组件在显示状态,如果setState(),则会进行8个组件重绘。实际用户只看到4个,其实应该也只需渲染4个, 且上下滑动也会触发屏幕外的Widget创建销毁,造成滚动卡顿。高性能的手机可预渲染,在低端机降级该区域距离为0或较小值。
      image.png​​​​​​​ 

五、包体积优化

  1. 资源压缩(图片、有图片的json、gif、音频、视频 等)
  2. 及时清理不使用、有重复的依赖库
  3. 启用代码缩减和资源缩减 (引用知乎

    打开 minifyEnabled 和 shrinkResources,构建出来的 release 包会减少 10% 左右的大小,甚至更多。

  4. 构建单 ABI 架构的包

    目前手机市场上,x86 / x86_64/armeabi/mips / mips6 的占有量很少,arm64-v8a 作为最新一代架构,是目前的主流,而 armeabi-v7a 只存在少部分的老旧手机中。

    所以,为了进一步优化包大小,你可以构建出单一架构的安装包,在 Flutter 中可以通过以下方式来构建出单一架构的安装包

    cd <flutter应用的android目录>
    flutter build apk --split-per-abi

    如果想进一步压缩包体积可将 so 进行动态下发,将 so 放在远端进行动态加载,不仅能进一步减少包体积也可以实现代码的热修复和动态加载。

六、其他优化(好的思维方式、sdk引进)

  1. 推荐一个http插件库,http插件库也支持构建一次连接多次请求的 方式,这样从网络层减少tcp握手和断开的消耗和时间的浪费。在需要一次性请求多个接口的时候会更能体现它的作用。
  2. 识别出消耗多余内存的图片​​​​​​​

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

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

  4. 着色器预热:着色器预热有助于提高路由切换时的流畅度,有关使用请查看着色器预热详情 
  5. ListView item 中有 image 的情况来优化内存

    通过将这两个选项置为 false 来禁用它们,这样不可见的子元素就会被自动处理和 GC。​​​​​​​​​​​​​​
    ListView.builder(
      ...
      addAutomaticKeepAlives: false (true by default)
      addRepaintBoundaries: false (true by default)
    );
    1. 由于重新绘制子元素和管理状态等操作会占用更多的 CPU 和 GPU 资源,但是它能够解决你 App 的内存问题,并且会得到一个高性能的视图列表。​​​​​​​​​​​​​​

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
课程首先介绍了Flutter的主要知识,通过4个案例消化吸收知识点。并通过手把手一步步带您完成一个【我的备忘录】APP项目,使得您真正认识Flutter在实际项目中的优势。  通过本课程学习您可以学习到Flutter技术如下知识:第1章 Flutter概述知识点:移动应用开发现状、移动应用开发类型、Hybrid与移动跨平台开发策略、Flutter是什么?、Flutter特点、Flutter体系结构。第2章 Flutter开发环境搭建知识点:基于Windows的Android开发环境、基于macOS的iOS开发环境、IDE开发工具设置。第3章 Flutter基础知识点:完成一个Flutter程序、一切都是组件(Widget)、组件分类、使用图片和图标资源、使用文本组件、增加调试组件工具。第4章 布局组件知识点:Flutter布局概述、容器布局(Container)、行(Row)、列(Column)布局、层叠布局、ListView、GridView。第5章 Material风格组件知识点:按钮、输入框、复选框、单选按钮、开关按钮、滑块。第6章 iOS Cupertino风格组件知识点:iOS Cupertino页面、Cupertino按钮、Cupertino开关按钮、Cupertino滑块、Cupertino分段控件。第7章 状态管理知识点:状态管理概述、局部状态管理、全局状态管理。第8章 导航知识点:导航概述、面包屑导航、标签导航、页面组件分散在不同文件中、全局状态管理与导航。第9章 工程依赖管理知识点:工程依赖管理概述、pub依赖管理工具。第10章 数据存储知识点:Flutter数据存储策略、键值对数据存储、文件数据存储、SQLite数据存储、示例:数据CRUD操作。第11章 网络通信知识点:搭建自己的Web服务器、使用http包、示例:城市信息列表。第12章 项目实战:我的备忘录APP知识点:备忘录APP项目说明、备忘录项目后台Web服务API说明、备忘录APP项目分析与设计、初始化工程、持久层实现、表示层实现。 

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值