Flutter性能优化
前言
App 流畅性的关键指标有 UI帧率,GPU帧率,我们期望它能达到 60fps,也就是16ms每帧。
以 profile / release 模式运行
为了获取最接近生产环境的数据,我们应该选择一台尽可能低端的真机,并且以 profile 模式或者 release 模式下运行app。
- 因为 debug 模式会有一些额外的检查工作,比如
assert()
等 - 为了加速开发效率,debug 模式是以
JIT(Just in time)
模式编译 dart 代码的, 而profile
和release
是提前编译为机器码AOT(Ahead Of Time)
,所以 debug 会慢很多。
所以说我们在查看性能时候不要用debug 模式,之前我就是用debug模式,无论怎么优化,性能都满足不了要求,还以为是flutter自身的问题,但是都说Flutter的渲染效率还是很高的,原来是debug模式的问题。
Flutter运行模式
-
Debug模式
调试页面开发时使用 -
Profile模式
调试性能 开发时使用 -
Release模式
部署发包时使用
Debug
Debug模式可以在真机和模拟器上同时运行,此模式会打开所有的断言,包括debugging信息、debugger aids(比如observatory)和服务扩展。优化了快速develop/run循环,但是没有优化执行速度、二进制大小和部署。
命令flutter run
就是以这种模式运行的,通过sky/tools/gn --android
或者sky/tools/gn --ios
来构建应用的。
Release
Release模式只能在真机上运行,不能在模拟器上运行:会关闭所有断言和debugging信息,关闭所有debugger工具。优化了快速启动、快速执行和减小包体积。禁用所有的debugging aids和服务扩展。这个模式是为了部署给最终的用户使用。
命令flutter run --release
就是以这种模式运行的,通过sky/tools/gn --android --runtime-mode=release
或者sky/tools/gn --ios --runtime-mode=release
来构建应用。
Profile
Profile模式只能在真机上运行,不能在模拟器上运行,基本和Release模式一致,除了启用了服务扩展和tracing,以及一些为了最低限度支持tracing运行的东西(比如可以连接observatory到进程)。
命令flutter run --profile
就是以这种模式运行的,通过sky/tools/gn --android --runtime-mode=profile
或者sky/tools/gn --ios --runtime-mode=profile
来构建应用。
test
headless test模式只能在桌面上运行,基本和Debug模式一致,除了是headless的而且你能在桌面运行。
命令flutter test
就是以这种模式运行的,通过sky/tools/gn
来build。
怎么使用profile模式呢?
为了调试性能问题,我们需要在发布模式的基础之上,为分析工具提供少量必要的应用追踪信息,这就是分析模式。
除了一些调试性能问题必须的追踪方法之外,Flutter 应用的分析模式和发布模式的编译和运行是类似的,只是启动参数变成了 profile 而已。
我们可以在 Android Studio 中通过菜单栏点击 Run
=>Profile
=>main.dart
选项启动应用,
也可以通过命令行参数 flutter run --profile
运行 Flutter 应用。
Android Studio
菜单栏点击 Run
=> Profile...
注意
该过程第一次编译非常慢请耐心等待,后来就会快很多。
VSCode
打开 launch.json
文件并设置flutterMode
为 profile
"configurations": [
{
"name": "Flutter",
"request": "launch",
"type": "dart",
"flutterMode": "profile" # 测试完后记得把它改回去!
}
]
命令行
flutter run --profile
检测帧率
Android Studio中配置
File=>Settings
中搜索flutter
找到
打开Open Flutter Inspector view on app launch
选中 View > Tool Windows > Flutter Performance
.
第一个按钮会在应用中显示,最后按钮一个会减速,方便我们查看帧率
VS Code中配置
选中 View > Command Palette…
会显示一个 command 面板.
在命令面板中输入 performance
并选择 Toggle Performance Overlay
如果命令显示为不可用,需要检查 app 是否正在运行.
代码中配置
在 MaterialApp
或者 WidgetsApp
的构造函数中设置 showPerformanceOverlay
属性为 true
:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showPerformanceOverlay: true, // 开启
title: 'My Awesome App',
home: MyHomePage(title: 'My Awesome App'),
);
}
}
怎么看帧率
上图演示了性能图层的展现样式。其中,GPU 线程的性能情况在上面,UI 线程的情况显示在下面,蓝色垂直的线条表示已执行的正常帧,绿色的线条代表的是当前帧。
同时,为了保持 60Hz 的刷新频率,GPU 线程与 UI 线程中执行每一帧耗费的时间都应该小于 16ms(1/60 秒)。
在这其中有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条,如下图所示。
如果红色竖条出现在 GPU 线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了 UI 线程图表,则表示 Dart 代码消耗了大量资源,需要优化代码执行时间。
图中有三条线,最下面的一条线为16ms,如果应用大部分都在16ms下,就优化的差不多了。
图表分别体现了 UI帧率 和 GPU帧率。如果出现了红色,说明对应的线程有太多work要做。
那先来了解一下 Flutter 中的4个主要线程分别承担了什么职责。
- Platform线程:插件代码运行的线程;即Android/iOS的主线程,
- UI线程:在Dart虚拟机中执行Dart代码。作用是创建视图树,然后将它发送给GPU。注意不要阻塞此线程!
- GPU线程:把上面提到的视图树渲染出来,虽然我们在flutter中不能直接访问GPU线程和数据,但是Dart代码可能导致此线程变慢
- I/O线程:执行比较耗时的任务
在运行app的过程中,观察爆红的地方和触发场景,进行分析。
如果是UI报红:
那么可能是执行了某个较耗时的函数?或者函数调用过多?算法复杂度高?
如果只是 GPU 报红:
那么可能是要绘制的图形过于复杂?或者执行了过多GPU操作?
- 比如要实现一个混合图层的半透明效果:如果把透明度设置在顶层控件上,CPU会把每个子控件图层渲染出来,再执行
saveLayer
操作保存为一个图层,最后给这个图层设置透明度。而saveLayer
开销很大,这里官方给出了一个建议:首先确认这些效果是否真的有必要;如果有必要,我们可以把透明度设置到每个子控件上,而不是父控件。裁剪操作也是类似。 - 还有一个拖慢GPU渲染速度的是没有给静态图像做缓存,导致每次build都会重新绘制。我们可以把静态图形加到
RepaintBoundry
控件中,引擎会自动判断图像是否复杂到需要用repaint boundary,不需要的话也会忽略。 - 开启saveLayer和图形缓存的检查 MaterialApp( showPerformanceOverlay: true, // 使用了saveLayer的图形会显示为棋盘格式并随着页面刷新而闪烁 checkerboardOffscreenLayers: true, // 做了缓存的静态图片在刷新页面时不会改变棋盘格的颜色;如果棋盘格颜色变了说明被重新缓存了,这是我们要避免的 checkerboardRasterCacheImages: true, );
提高流畅性的策略
- 代码调用时机是否可以延后?如底部导航栏式的页面,没有必要第一次进入就把每个子Page都创建出来
- 尽量做到局部刷新
- 把耗时的计算放到独立的isolate去执行
- 检查不必要的 saveLayer
- 检查静态图片是否添加缓存
- relayout boundary:参考
- repaint boundary:参考
内存优化
在内存优化方面,我们的目标是希望减少应用内存占用,减少被系统杀死的概率,同时尽可能的避免内存泄露,减少内存碎片化。
内存优化策略
- 加载对象过大?如图片质量和尺寸不做限制就加载
- 加载对象过多?如加载长列表;在调用频率很高的方法中创建对象
- 合理设置缓存大小/长度
- 在内存不足时或离开页面时清空缓存数据
- 使用ListView.build()来复用子控件
- 自定义绘图中避免在onDraw中做创建对象操作,或者相同的参数设置
- 复用系统提供的资源,比如字符串、图片、动画、样式、颜色、简单布局,在应用中直接引用
- 内存泄露的问题?比如dispose需要销毁的listener等
- 不可见的视图是否也在build?
- 页面离开后的网络请求是否取消?
- Windows下安装MongoDB
- IIS Express魔法堂:解除localhost域名的锁定
- JavaSE(三)之static、final、abstract修饰符
- 为经典版eclipse增加web and JavaEE插件
- 协议森林07 傀儡 (UDP协议)
- 【设计模式】—— 访问者模式Visitor
- JAVA EE Eclipse下配置Tomcat服务器
- CentOS6.5菜鸟之旅:文件权限详解
- CMD魔法堂:支持显示UTF8编码的中文
- 【设计模式】—— 模板方法Template
- 【设计模式】—— 策略模式Strategy
- MyBatis魔法堂:即学即用篇
- 【设计模式】—— 状态模式State
- php环境无法上传文件的解决方法
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- LeetCode题目33:搜索旋转排序数组
- LeetCode题目34:在排序数组中查找元素的第一个和最后一个位置
- LeetCode题目35:搜索插入位置
- LeetCode题目36:有效的数独
- 你必须掌握动态规划——LeetCode题目5:最长回文子串
- 有意思的难题——LeetCode题目37:解数独
- 源码分析-分布式链路追踪:Skywalking存储插件能力-elasticsearch
- mongodb 4.0副本集搭建
- 浅析Kubernetes Pod重启策略和健康检查
- SpringBoot2 整合Ehcache组件,轻量级缓存管理
- 数据源管理 | 分布式NoSQL系统,Cassandra集群管理
- 【NPM库】- 0x03 - Express
- 数值微分|多项式的导数计算
- 让windows 10 内置ubuntu(WSL)成为扩增子分析生产力
- 手把手教你自定义Spring Boot Starter