最近做了几年的flutter开发,跟随flutter版本从1.*到现在的3.*的项目迭代中,也逐渐开始着手项目的性能优化体验,一些基础的flutter垃圾回收机制(GC)的原理这里也不再过多的解释,想必相关的开发人员也早有些耳闻,不是很清楚的也可以去网络上查看大神的一些原理性文章。这里就不过多赘述。
开发检测工具我单独出了一篇文章flutter 优化检测工具https://blog.csdn.net/BUG_delete/article/details/129295442,有需要可以去查阅参考。
本篇文章随着我的理解加深和新的技术学习,会不断的升级补充新内容。有喜欢flutter的攻城狮们可以点赞收藏本文章,以供之后及时的查看新的内容更新。
下面,我稍微总结一些自己开发flutter的项目所遇到的优化方式和方法。以供有心之人共勉。
一、内存优化
- 超出作用范围及时释放。
- 注意listener、provider等一系列对象的关闭和移除操作。
- 减少单例模型的创建和使用
- 不一定是越少越好,最主要的原则是:在退出它可使用的最小范围之后能够及时的销毁。
- provider的value形式也是单例。
- 避免异步使用context
-
-
-
图片的加载优化
-
我们只用图片的时候flutter的相关图片加载构造器里面都会有width、height的参数,尽量在使用图片的地方都加上参数的设置。flutter的内部会将width、height作为缓存cache这个图片的时候的大小参数,会将图片repaintsize之后作为缓存这个图片,这就避免大图片(使用的其实不是很大的尺寸)给设备内存造成的不必要的压力问题。
-
- 及时清理(GC回收机制并没有RAC机制那么流畅,回收操作感觉并没有实时在做,就需要我们在不使用的时候及时移除占用)
- 图片清理:PaintingBinding.instance.imageCache.clear();
- webview清理缓存:_webViewController?.clearCache();注意这里的清理会清理项目中所有webview的local存储,并非只是当前webview所加载的页面
- Lottie清理:sharedLottieCache.clear(); (需引入路径:import 'package:lottie/src/providers/lottie_provider.dart';可能目前1.4.3版本比较奇怪引入lottie总文件路径无效)
二、启动速度优化
- 避免启动初始化过多耗时任务(分块加载初始化)
- eg:网络初始化可以放在闪屏页面,和用户强关联的模块、sdk可以放在登录之后操作。
- 页面启动耗时避免initState方法里面做。
- 任务放在状态管理机制里面处理,页面启动“友好交互”原则(加载动画、或先展示旧数据等新数据处理完成之后再刷新)
- 页面build里面有耗时操作
-
我们应该尽量避免在 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 来进行异常处理,这能够帮助你处理一些数据解析方面的异常。
- Flutter 引擎预加载(引用知乎)
- 使用它可以达到页面秒开的一个效果,具体实现为:
在 HIFlutterCacheManager 类中定义一个 preLoad 方法,使用 Looper.myQueue().addIdleHandler 添加一个 idelHandler,当 CPU 空闲时会回调 queueIdle 方法,在这个方法里,你就可以去初始化 FlutterEngine,并把它缓存到集合中。
预加载完成之后,你就可以通过 HIFlutterCacheManager 类的 getCachedFlutterEngine 方法从集合中获取到缓存好的引擎。
- 使用它可以达到页面秒开的一个效果,具体实现为:
- Dart VM 预热
- 对于 Native + Flutter 的混合场景,如果不想使用引擎预加载的方式,那么要提升 Flutter 的启动速度也可以通 过Dart VM 预热来完成,这种方式会提升一定的 Flutter 引擎加载速度,但整体对启动速度的提升没有预加载引擎提升的那么多。
无论是引擎预加载还是 Dart VM 预热都是有一定的内存成本的,如果 App 内存压力不大,并且预判用户接下来会访问 Flutter 业务,那么使用这个优化就能带来很好的价值;反之,则可能造成资源浪费,意义不大。
- 对于 Native + Flutter 的混合场景,如果不想使用引擎预加载的方式,那么要提升 Flutter 的启动速度也可以通 过Dart VM 预热来完成,这种方式会提升一定的 Flutter 引擎加载速度,但整体对启动速度的提升没有预加载引擎提升的那么多。
-
7、让 Flutter 中重建组件的个数尽量少
-
在实际开发过程中,如果将整个页面写在一个单独的 StatefulWidget 中,那么每次状态更新时都会导致很多不必要的 UI 重建。因此, 我们要学会拆解组件,使用良好设计模式和状态管理方案,当需要更新状态时将影响范围降到最小。
-
-
8、Flutter实现的一些效果背后可能会使用 saveLayer() 这个代价很大的方法
-
如下这几个组件,底层都会触发 saveLayer() 的调用,同样也都会导致性能的损耗:ShaderMask,ColorFilter,Chip(当 disabledColorAlpha != 0xff 的时候,会调用 saveLayer())、Text(如果有 overflowShader,可能调用 saveLayer() )。
-
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 那么耗时,但仍然很耗时,所以请谨慎使用。
-
10、代码中尽量不要使用microtask(Future.microtask,microtask 内部队列主要是由 Dart 内部产生,而 Stream 中的执行异步的模式就是 scheduleMicrotask 了)来执行任务,因为microtask的优先级高于even queue(点击、滑动、IO、绘制事件 等事件),
三、布局加载优化
- 避免整体刷新setState,(除非Widget页面元素较少,或者确实需要很多元素更新)
- 可以使用dart的StreamBuilder、ValueListenableBuilder、GlobalKey等操作局部刷新。
- 绘制完成的图表CustomPainter的组件,滑动期间避免重绘问题。(RepaintBoundary)或者灵活处理shouldRepaint方法
@override bool shouldRepaint(CustomPainter oldDelegate) { return oldDelegate != this; }
- 元素最小化原则
- 避免清一色使用Container,适当的使用SizeBox,DecoratedBox,Padding来减少组件的臃肿性。
- 使用nil来代替不必要的占位元素(SizeBox或者Container),你需要明白 nil 仅仅是一个基础的 Widget 元素 ,它的构建成本几乎没有。一般我们在三目运算符的时候会用的比较多。
- 尽可能地使用 const,抑制 widget 的重建
- 如果某一个实例已经用 const 定义好了,那么其它地方再次使用 const 定义时,则会直接从常量池里取,这样便能够节省 RAM。针对一些长期不修改的组件更应该使用。
- const不仅适用于字符串、数字的常量定义也适用于widget的构建
- build() 方法中的嵌套漩涡(这个本身没有问题)存在的问题:
- 代码可读性差:画界面时需要一个 Widget 嵌套一个 Widget,但如果 Widget 嵌套太深,就会导致代码的可读性变差,也不利于后期的维护和扩展。
- 复用难:由于所有的代码都在一个 build(),会导致无法将公共的 UI 代码复用到其它的页面或模块。
- 影响性能:我们在 State 上调用 setState() 时,所有 build() 中的 Widget 都将被重建,因此 build() 中返回的 Widget 树越大,那么需要重建的 Widget 就越多,也就会对性能越不利。
-
所以,你需要 控制 build 方法耗时,将 Widget 拆小,避免直接返回一个巨大的 Widget,这样 Widget 会享有更细粒度的重建和复用。
-
实际上可以将Build函数视为仅渲染UI的方法,实际情况也理所应当这样做。
- 合理使用StatelessWidget和StatefulWidget
- StatelessWidget更加轻量
- 避免使用List.forEach进行大批量的处理
- 实际上Future是个很好的东西,比如Future.forEach同样可以遍历数组但在执行的过程中,对主线程带来的影响确是有差异的。
- 非必需,列表形态的可以使用懒加载(按需加载):builder的方法来加载。
- eg:ListView.builder()、GridView.builder(),PageView.builder()
- 多变图层与不变图层分离
- 如Gif,动画。这时我们就需要RepaintBoundary,不过独立图层合成也是有消耗,这块需实测把握。这会导致页面同一图层重新Paint。此时可以用RepaintBoundary包裹该多变的Gif组件,让其处在单独的图层,待最终再一块图层合成上屏。页面滑动将不会导致动画重绘。还可以用该widget包住我们的图片,做图片缓存,ListView里面就使用了RepaintBoundary来包住item,缓存item。
- 如Gif,动画。这时我们就需要RepaintBoundary,不过独立图层合成也是有消耗,这块需实测把握。这会导致页面同一图层重新Paint。此时可以用RepaintBoundary包裹该多变的Gif组件,让其处在单独的图层,待最终再一块图层合成上屏。页面滑动将不会导致动画重绘。还可以用该widget包住我们的图片,做图片缓存,ListView里面就使用了RepaintBoundary来包住item,缓存item。
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。
四、关键指标(页面异常率,页面帧率,页面加载时长等)
- 列表优化
- 对于长列表,记得在 ListView 中使用 itemExtent。有时候当我们有一个很长的列表,想要用滚动条来大跳时,使用 itemExtent 就很重要了,它会帮助 Flutter 去计算 ListView 的滚动位置而不是计算每一个 Widget 的高度,与此同时,它能够使滚动动画有更好的性能。
- 减少可折叠 ListView 的构建时间。 针对于可折叠的 ListView,未展开状态时,设置其 itemCount 为 0,这样 item 只会在展开状态下才进行构建,以减少页面第一次的打开构建时间。
- 尽量不要为 Widget 设置半透明效果
- 考虑用图片的形式代替,这样被遮挡的部分 Widget 区域就不需要绘制了。
- 优化光栅线程
- 用 key 加速 Flutter 的性能优化光栅线程
- 低端设备特殊适配
-
降级CustomScrollView,ListView等预渲染区域为合理值
默认情况下,CustomScrollView除了渲染屏幕内的内容,还会渲染上下各250区域的组件内容,例如当前屏幕可显示4个组件,实际仍有上下共4个组件在显示状态,如果setState(),则会进行8个组件重绘。实际用户只看到4个,其实应该也只需渲染4个, 且上下滑动也会触发屏幕外的Widget创建销毁,造成滚动卡顿。高性能的手机可预渲染,在低端机降级该区域距离为0或较小值。
-
五、包体积优化
- 资源压缩(图片、有图片的json、gif、音频、视频 等)
- 及时清理不使用、有重复的依赖库
-
启用代码缩减和资源缩减 (引用知乎)
打开 minifyEnabled 和 shrinkResources,构建出来的 release 包会减少 10% 左右的大小,甚至更多。
-
构建单 ABI 架构的包
目前手机市场上,x86 / x86_64/armeabi/mips / mips6 的占有量很少,arm64-v8a 作为最新一代架构,是目前的主流,而 armeabi-v7a 只存在少部分的老旧手机中。
所以,为了进一步优化包大小,你可以构建出单一架构的安装包,在 Flutter 中可以通过以下方式来构建出单一架构的安装包:
cd <flutter应用的android目录> flutter build apk --split-per-abi
如果想进一步压缩包体积可将 so 进行动态下发,将 so 放在远端进行动态加载,不仅能进一步减少包体积也可以实现代码的热修复和动态加载。
六、其他优化(好的思维方式、sdk引进)
- 推荐一个http插件库,http插件库也支持构建一次连接多次请求的 方式,这样从网络层减少tcp握手和断开的消耗和时间的浪费。在需要一次性请求多个接口的时候会更能体现它的作用。
-
识别出消耗多余内存的图片
-
Flutter Inspector:点击 “Invert Oversized Images”,它会识别出那些解码大小超过展示大小的图片,并且系统会将其倒置,这些你就能更容易在 App 页面中找到它。
针对这些图片,你可以指定 cacheWidth 和 cacheHeight 为展示大小,这样可以让 flutter 引擎以指定大小解析图片,减少内存消耗。
- 着色器预热:着色器预热有助于提高路由切换时的流畅度,有关使用请查看着色器预热详情
-
ListView item 中有 image 的情况来优化内存
通过将这两个选项置为 false 来禁用它们,这样不可见的子元素就会被自动处理和 GC。ListView.builder( ... addAutomaticKeepAlives: false (true by default) addRepaintBoundaries: false (true by default) );
-
由于重新绘制子元素和管理状态等操作会占用更多的 CPU 和 GPU 资源,但是它能够解决你 App 的内存问题,并且会得到一个高性能的视图列表。
-