Flutter 的性能分析和监控
一、devTools 的使用
devTools 是官方出的一套 Dart 和 Flutter 的性能调试工具,其核心是帮开发者定位 UI 或者 GPU 线程问题,从而协助开发者解决 Flutter App 的性能问题。在应用该工具之前,需要启动 App 调试功能。
打开 Flutter 项目,如果使用的是 Android Studio IDE,可以直接在工具栏中点击如下图所示的红色圈部分。
如果不是在 Android Studio 中,需要按照下面的四个步骤启动 devTools 工具。
1.使用下面命令启动 devTools 工具。
1 | flutter pub global run devtools |
2.运行成功后,会提示 devTools 访问的地址。打开访问地址后,可以看到如下图的界面,界面中需要输入一个 Flutter App 的监听地址。
3.接下来需要获取 Flutter 运行的 WS 地址,重新运行项目(请注意不是热启动,需要停止运行,然后点击重新运行),启动成功后,在运行栏可以看到如下所示的信息。
1 | Launching lib/main.dart on iPhone 14 Pro Max in debug mode... |
4.将其中的 listening on 的地址输入到刚才 devTools 页面就可以了,打开页面后,可以看到下图所示的功能。
接下来介绍下 devTools 功能,即上图中每个工具的作用。
- Flutter Inspector,可以查看组件的布局信息,类似于前端的 Chrome 工具的 CSS 布局查看器,应用该功能可以快速定位布局问题。
- Timeline,时间线事件图表,跟踪显示来自应用程序的所有事件。监听 Flutter App 在构建 UI 树,绘制界面以及其他(例如 HTTP 流量)事件等,并将监听到的事件所花费的时间,显示在时间轴上。
- Memory,使用时间线的方式,展示 Flutter App 的内存变化,通过该工具可以定位内存泄漏的问题。
- Performance,性能分析工具,可以通过录制界面操作,获取界面性能数据。该工具的主要用途还是在定位某个功能卡顿问题,例如我们发现主界面很卡顿,这时候就可以通过该工具录制首页加载过程,然后分析出具体性能异常逻辑。
- Debugger,断点调试功能,和 IDE 上的断点调试是一样的。
- Network,可以抓取网络请求,并分析返回数据,类似于前端 Chrome 的 Network 工具。
- Logging,运行期间的日志显示。日志中包含:Dart 运行时的垃圾回收事件、Flutter 框架事件,比如创建帧的事件、应用的 stdout 和 stderr 和应用的自定义日志事件。
1.1 应用 Timeline 来做性能分析和优化
Timeline 会记录每一帧的绘制,每一帧绘制又包括 UI 线程构建图形树和 GPU 线程绘制图像两个过程。在应用开发完成后,我们可以使用 Timeline 工具来走一遍 App 所有页面,记录每一帧的性能耗时。请注意该功能最好是在外接实体机上进行测试,不然会出现数据相差较大。我们分为以下七个步骤来进行实践。
- 连接实体调试机器,然后运行 flutter run –profile 启动 App。
- 打开 devTools 工具,点击 Timeline 工具,点击 Clear 清空旧数据。
- 可以在某个页面上进行一系列的基础操作,操作完成后,回到 devTools 中,点击 Refresh,这时候会有一个短暂的分析过程,分析完成后,你会看到下图所示的内容。
- 在界面中,你会看到浅蓝( UI 线程耗时,小于 16.67ms)、深蓝( GPU 耗时,小于 16.67 ms)、橘黄( UI 线程耗时,大于 16.67 ms)和深红( GPU 耗时,大于 16.67 ms)的柱状数据,浅蓝和橘黄都代表 UI 线程耗时,深蓝和深红都代表 GPU 耗时,在 UI 线程耗时和 GPU 耗时都小于 16.67 ms 时显示浅蓝和深蓝,而当 UI 线程耗时或者 GPU 耗时任意一个大于 16.67 ms 时,则显示橘黄和深红。
- 当发现有橘黄和深红的柱状图时,则需要进行具体的分析,这时候只需要点击这部分柱状图,就可以看到下图所示的一个效果。
- 如果 UI 线程耗时比较长,点击具体较长的柱状图,可以看到具体的火焰图。如下图所示,其中的宽度就代表执行的时间长度,宽度越长表明性能损耗越大,而这就是性能优化的部分。
- 如果 GPU 耗时较长,则可以往下拉查看 GPU 页面绘制问题,如下图所示。
接下来我们就分别从 UI 线程问题和 GPU ( Raster )来分析具体的性能问题。
1.2 UI 线程问题实践分析
如果出现橘黄色和深红色的柱状图时,我们需要单独分析这块的性能问题。大部分情况是因为在 Dart 中执行了比较耗时的函数,或者在组件树设计上没有注意性能导致的问题。这里介绍下可能会提升或者影响性能的几个关键点。
- 不会发生任何变化的组件,使用 const ,减少绘制,例如我们的通用 loading 组件。
- 减少组件绘制,这点就是我们之前提到的尽量减少有状态组件下的子组件,或者通过状态管理模块 Provider 来辅助管理状态。
- 复杂业务 build 函数在代码逻辑中,避免复杂业务在 build 逻辑中去执行。
1.3 GPU( Raster )
一般情况下都是较复杂的图片绘制产生的问题,比如说复杂的动效或者复杂的图片资源。上面的工具也不能完全帮你定位到异常的问题。需要根据实际的代码逻辑来分析,这点是比较困难的,只能排除法步步寻找问题点。Timeline 图只能协助我们去找到 GPU 存在性问题。
在遇到 GPU 问题时,可以在 devTools 中的 Performance 打开 Performance Overlay 工具,打开后在真机或者虚拟机上就可以看到效果,当出现 GPU 性能问题时,会出现红色线条。
以上就是 devTools 的工具使用,通过这个工具可以大大提升我们定位问题的效率。
二、性能上报
为了能够更好地分析和判断性能问题,我们有时候需要采集现网运行期间的一些性能数据,例如我们需要主要的两个指标:Crash 率和 FPS 数据。接下来我们主要介绍下如何计算和采集这两个数据的方法。
由于这部分肯定会影响主线程的性能,因此我们将该计算和上报过程放入到一个新的线程去处理,避免影响主线程。这里就需要用到Isolate 线程进行双向的通信;
2.1 Crash 率
异常率的计算方法是需要根据手机机型和手机版本来进行分析,我们先制定如下数据指标:
- 机型的 Crash 率 = 机型的 Crash 量 / 该机型页面访问量
- 版本的 Crash 率 = 版本的 Crash 量 / 该版本页面的访问量
- 版本机型的 Crash 率 = 机型版本的 Crash 量 / 该机型特定版本的访问量
根据上面的计算方式,我们需要增加一些数据上报,主要包括:机型、版本、页面名称、Crash 情况。
2.2 FPS
计算 FPS 的逻辑相对来说较复杂一些,首先需要使用 Flutter 的 SchedulerBinding.instance.addTimingsCallback 函数来获取每一帧耗时,这段代码主要是在 Flutter 绘制完成每一帧后都会进行回调处理,通过回调的方式可以采集到每一帧的耗时信息,具体代码逻辑如下:
1 | /// 启动监听数据 |
然后在 onReportTimings 中将每一帧数据分别保存到 frames 和 routerFrames ,代码如下:
1 | /// 数据处理 |
routerFrames 为当前页面路由的帧耗时的队列,frames 为所有帧耗时的队列。有了绘制的每一帧数据后,我们再将数据传递到其他线程进行计算,这里会传递到 IsolateHandle 的 calculateFps 方法,我们具体看下这个方法的计算逻辑。
1 | /// 计算当个页面的 fps |
上述代码中,首先获取标准的一帧绘制时间 16.67 ms(目前这部分是hardcode 60 HZ,后续需要匹配 120 HZ),然后分别计算每一帧的渲染耗时情况,并与 16.67 ms 进行对比,得到掉帧数量。计算掉帧的方式是,用耗时时间除以 16.67 ms 下取整就代表掉帧数量。比如耗时 34 ms,代表掉帧了 2 帧,因为 34 / 16.67 = 2.039。最后用以下公式来计算 FPS 。
1 | (list.length * 60) / (list.length + lostNum) |
FPS 和 PV 一样将数据上报到服务端,后续在服务端进行分析。
以上就完成了所有的性能上报功能,接下来我们在某个页面进行尝试,这里选择之前侧边栏的“单图片信息”。
2.3 应用
在该类中的 initState 中上报 PV ,并在页面开始加载前,将帧放入到具体的 routerFrames 中,代码如下:
1 |
|
其中Report.startRecord('${this.runtimeType}');
就是上报 PV ,并开始记录 routerFrames ,这里通过 this.runtimeType 可以获得具体的类名。FPS 则在页面最后一帧加载完成后回调,然后在回调中计算 FPS 相关数据。在 Flutter 提供了接收帧绘制完成后回调的方法,需要在 build 逻辑中增加下面的代码。
1 | WidgetsBinding.instance.addPostFrameCallback( |
然后在 Report.endRecord 调用其他线程函数,计算 FPS,并需要清空 routerFrames 。
1 | /// 结束并显示数据 |
完成后就可以在虚拟机或者真机上进行模拟测试了,不过这里的 FPS 数据不一定完全准确,后续需要进一步优化。