Flutter 的可视化界面、绘制性能调优 🔧 —— DevTools

9,357 阅读14分钟

前言

给大家介绍DevTools的主要原因主要有几点 🚀 :

  • 首先是DevTools本身是Flutter官方推荐的一个调试工具。
  • DevTools是用Flutter编写的,极具特色 🖼 。
  • 拥有非常全面的调试功能,可以满足大小、方面不同的优化需求 ⚙️。

DevTools

首先,隆重介绍今天的主角:DevTools 👏

官网介绍:DevTools is a suite of performance and debugging tools for Dart and Flutter. It’s currently in beta release, but is under active development.

翻译:DevToolsDartFlutter的一套性能和调试工具。它目前处于测试版,但正在积极开发中。

安装 🔌

要使用DevTools首先,所有的一切的前提,肯定是,装上它,yes,这看似简单的一步其实花了我还蛮久时间的。希望各位安装顺利,我将给各位介绍四种打开DevTools的方法

Android Studio / Intellij

使用以上两种开发工具的小伙伴看这边了 🙋‍♂️,在此类开发工具上打开DevTools一共分三个步骤

  1. 安装 Flutter 插件: 首先确认你已经在开发工具内安装了Flutter 插件(不会有人还没装吧,不会吧 👀 )



  2. 开始调试一个应用 🔩 对,就是字面意思,允许你要调试的那个项目工程(推荐使用Profile模式)。友情提示:Profile模式只能在真机 📱 上运行。

  3. 找到 DevTools 的入口 其实 DevTools 的入口不是很显眼,但是还算好找,在底部的工具栏,咱们经常用的hot reload边上,直接点击该按钮 🔘 就可以启动该项目的DevTools

(PS:我自己用的是Android Studio,但是我并没有通过这种方式成功,一直就是Installing DevTools...,如果各位知道原因,欢迎在评论区指点一下,阿里嘎多 🤗 )

VS Code

  1. 安装 Flutter 插件: 首先确认你已经在开发工具内安装了FlutterDart 插件(同Android Studio


  2. 通过在 VS Code 中打开你的项目的根目录(包含 pubspec.yaml)并点击 Run > Debugging (F5),来开启调试会话。

  3. 启动开发程序

  • 一旦调试会话处于活跃且应用程序已开启 🏃🏻 ,那么 VS Code 命令控制板中将会显示 Dart: Open DevTools

  • 当你第一次运行时(以及未来更新开发工具包时),系统会提醒你激活或升级DevTools

Command Line (命令行)

  1. 安装开发者工具 如果在你的环境变量 PATH 中有 pub, 可以运行 🚀:
pub global activate devtools

如果环境变量 PATH 中有 flutter , 可以运行 🚀:

flutter pub global activate devtools
  1. 启动开发者工具服务 下一步,启动本地 web server 服务来运行开发者工具。运行下面两个命令中的一个。
pub global run devtools   # If you have `pub` on your path.
flutter pub global run devtools   # If you have `flutter` on your path.

运行这两行代码之后,在命令行应该会有这样一个输出:

An Observatory debugger and profiler on iPhone X is available
at: http://127.0.0.1:50976/Swm0bjIe0ks=/

at后面的就是DevTools的地址,大家直接用Chrome打开即可。打开后的网页需要你填写一个链接,也就是你需要调试的项目的Debug service监听地址,这个地址哪儿来的呢 🤔 ?



3. 启动一个 appdebug

其实上面所需的链接 🔗 地址,就在我们平时运行项目的Log中。大家运行工程,在日志的前几行就可以找到Debug service的地址,就是图中 listening on之后的部分,将该链接填入我们在第二步打开的网页中,就可以进入到我们项目对应的DevTools之中。

暴力打开

以上三种是Flutter官网提供的安装、打开方式,很可惜,我都没有打开成功 🤯 ,不是一直安装,就是打开了一个非常老的版本的DevTools,应该是和我的Dart SDK路径配置有关,所以经过我的研究,发现了一个非常痛快 🤫 的打开方法(目前暂不知有什么弊端)。

  1. 找到Flutter文件夹的地址。 这个因人而异,各位应该清楚自己电脑上flutter的路径吧,比如我就放在个人的development目录下:
/Users/tys/development/flutter
  1. 找到DevTools 这就是我这边要说的暴力打开的关键 🔑 ,我发现...,这个DevTools,好像已经悄悄的放在了Flutter中。
 /Users/tys/development/flutter/.pub-cache/bin/devtools

在上面这个目录下,大家可以直接找到它,双击点开,直接好家伙 💡



芜湖~直接到达上面用命令行打开的最后一步。而且这就是你当前使用的Flutter版本对应的最新的、可用的DevTools

使用 DevTools🕵🏻‍♂️‘

我将按照DevTools顶部工具栏的顺序给各位逐个介绍各个功能 🤓 。

Flutter inspector

  • 简介:这一部分的功能主要是面向各位的界面模块,里面首先会展示你整体的布局绘制树 🌲 ,点击树的每个节点都会在右侧显示更细节的布局信息 🐳 ,同时,在布局信息上你可以看到可视化的视图信息并进行一些简单的选择操作,这部分功能用官方的话说主要有两个目的:   1. 了解现有布局。
      2. 诊断布局问题。

      有关这个模块的内容我们根据下图中红色标注的顺序一起看一下 🔍

Select widget mode

  这其实是一个选择视图,大家看到这个Tab下对应的其实就是你的整个绘制树 🌲 了,这样一颗树的结构复杂度一般都会和你的页面复杂度相关。如果是一个复杂的页面结构,这棵树也会非常庞大,看起来会很模糊,为了让我们可以更好的看到树里面的细节结构,我们就可以点击树上的某个节点,区查看树的某一部分的详细内容 🔮 。

Detail Tree

  顾名思义,其实这里就是我们刚才所选择的节点的详细信息,其实现实的信息真的可以说非常走心 ❤️ 了。看图中,我选了一个Container节点 ,Container下的未赋值属性都给你提示出来了,如bg背景颜色等。如果你的页面内有颜色、色彩的叠加之类的,通过它可以看的很清晰 🔬 。

Layout Explorer

   这个就厉害了,是你当前选中节点的布局浏览器。比如大家可以看到,我选中的这个Container下面有包裹的详细结构都有显示出来,包括各个Wiget的边界,甚至给你显示了计算后的高度和宽度 📏 (按照标准的分辨率尺寸)。


  Layout Explorer不仅能让你查看整个布局界面,还可以让你做一些简单的动态操作,让你在不改动代码的前提下,明确你的布局问题,或者缺陷(请看动图⬇️ )。

Slow Animations

  点击该按钮,将降低你App内的所有动画效果的速度,感受0.5倍速的神奇世 🤩 ,这里说的Animations包括但不仅限于你的界面跳转,Hero等系统级动画,个人认为这部分功能主要有两个作用

   1. 如果在该模式下你的动画不存在任何卡顿,则说明你的动画效能非常完美,可以达到比较好的用户体验 👍 。
   2. 让部分动画制作者看清自己的动画绘制曲线、路径 🕳 。

Debug Paint

  在渲染中添加视觉调试提示,以显示边框,填充,对齐和间隔。会在你的模拟器上显示整体的布局情况,这些情况包括你的整体渲染方向和基础组件类。

Paint Baselines

  使每个RenderBox在其每个文本基线处绘制一条线。就是下图中的绿线 ⬇️

Repaint Rainbow

  打开这个功能,你的界面在重绘时,会在重绘的部分更改一个边界颜色 🎆 。比如,你有一个banner,间隔1s换一张图片,那么每一秒你的banner图周边的颜色就会变一个随机颜色。个人感觉其目的是让我们看到当前页面正在绘制或重绘的部分。

Timeline 🌟

在进行性能调试时请使用实际设备调试。Skia有两套很不同的后端,Flutter在iOS模拟器中使用纯CPU后端,而在实际设备一般使用GPU硬件加速后端,所以性能特性很不一样。 TimeLineDevTools中比较实用有实际意义的部分。可以让你实际看到你的APP存在的UI问题或GPU问题。接下来我将带各位来学习如何去分析自己Flutter APP的绘制性能 🩺 。


  大家首先可以看到,在上图中最上层的部分是一个柱形图 📊 。这部分是你刚才所做操作的一个绘制状态图,根据右边的图例,也能明白其中的意思,这边我再给各位明确一下。

UI、Raster、Jank

  大家看柱状图中有红色有蓝色,淡蓝色的是UI线程的绘制情况、深蓝色是你的栅格化线程(GPU线程)。红色,代表你的绘制出现了卡顿。一般来说我们在Flutter里我们定义卡顿:如果一帧的渲染时间超过16ms,则会被认为此帧是延时的,为了达到帧渲染频率到 60 FPS (每秒帧数),每一帧的渲染时间必须等于或少于 16 ms。如果没有达到这个目标,你会发现 UI 不流畅或丢帧 📌 。

Timeline Event

  我们在整个界面的中间部分可以看到的是Timeline Event,它其实对应的就是我们上面绘制情况图的细节情况,是绘制过程中的事件。我们利用它可以详细的看见我们卡顿的原因。我们挑选其中卡顿的一帧,来仔细分析一下 🧮 。
  在下图中大家可以看到在Timeline Event中,依然分为了两个部分,上半部分是UI Event,下半部分是Raster Event。在这一阵中,我们可以明显的感觉到的是,我们的UI Event占用了非常长的时间 ⏰ 。为了大家能对整个TimeLine Event的含义有更好的理解,这里引用了官方的一段解释:

火焰图选项卡用于显示选中帧事件,CPU 的样本信息。图表展示的是自上而下的调用堆栈信息,即上面的堆栈帧调用下面的堆栈帧。每一个堆栈帧的宽度代表 CPU 执行的时长。栈帧消耗 CPU 的时间越长,就越洽有可能是我们进行性能改进的好地方。

  我们在上面的堆栈信息中可以看到最上层的一个EventVsyncProgressCallback,在下图中,我们可以在底部看到,这个堆栈的耗时高达40.6ms,几乎是无法接受的🚽 。其实我们点击一个流畅的帧后,发现其中也会有VsyncProgressCallback。那么为什么这一帧会格外的长,并造成卡顿呢?

  因此我们为了进一步探究其中的原因,我们需要进一步的深入探究,我们需要更多的信息。

  • 更详细的火焰图 🔥 (CPU Flame Chart)   这里有你想要的细节信息,包括内部具体某一个堆栈的耗时情况。为何VsyncProgressCallback耗时如此之久。内部有哪些细节操作,这里一清二楚 🐮 。这里我们可以看到,一个长耗时VsyncProgressCallback相比一个短耗时的VsyncProgressCallback多了几个部分。在清楚VsyncProgressCallback的内部堆栈信息后,我们就可以有针对性的再对内部的耗时堆栈进行分析。切换到我们第二个Tab

有关Flutter中的Vsync信号推荐阅读: Flutter渲染机制 - UI线程 🔗 。

  • 调用树   这部分更贴近我们开发者,我们可以详细看到VsyncProgressCallback具体调用了哪些函数,哪些函数是耗时比较久的。大家再结合上面一张图,我们在卡顿这一帧多做的工作,或者说多调用的函数,就是这里的performRebuild,点开函数的具体调用,我们可以发现,这个函数一直在循环调用。没错这里的performRebuild其实就是一个遍历你绘制树 🌲 的过程,说明了你在当前帧,更新了一个庞大的绘制树,并且树上的多个子节点也进行了重新绘制。所以我们应该要排查的就是,我们在界面中的哪个部分集中调用了多个组件的重绘,或者哪部分的view_mode,驱动了这样的一个重绘过程,其中是否有不必要的重绘。以此来提高渲染性能 😆 。

  上面一部分说明了我们的UI线程阻塞的部分原因,和排查方法,那如果是GPU呢?同样,我们选取了一帧典型的GPU耗时操作,在这个堆栈信息中我们可以看到我们的GPU方法确实占用了大量的时间 ⏳ ,同样是GPURasterizer::Draw为什么某一帧会特别久呢?

  其实在DevToolsRaster视图中,并不能将这部分展示的非常清晰。但是还有一个办法可以深究其原因。大家可以直接打开之前填入DevTools的链接。如果你正在profile模式下运行,你的log中就可以找到这个链接 🔗 。

  点开这个链接,在这个页面的中间部分,我们可以找到timeline工具(我已经在图中用红框标出)。

  进入这部分timeline之后,这里就是最最最最详细的每一帧的信息了 📌 。你可以用鼠标选中一块区域,在下方就会输出你选中区域的具体函数调用情况。包括函数名称、总耗时、调用次数、每次耗时以及一个可视化的柱状图 📊 占比情况。一目了然。有关GPU部分的函数调用需要各位对Flutter Engine的源码有比较高的理解,才能有针对性的解决问题。其中相关的调用栈非常的复杂。

  官方声明的会大量耗时的skia函数调用:

  • saveLayer: 非常耗时,每次调用需要在CPU里重新分配一块绘图缓冲区,并且告诉GPU切换绘图目标。尤其在比较老的设备上。
  • clipPath: 耗时,每次调用,会影响接下来每一个绘图指令,当绘图指令复杂时,会做多次和clipPath的相交操作,把在clipPath之外的部分剔除。

  可能大家会说,这部分函数,平时在编程的时候几乎没有使用 🤯 ,但其实他们广泛存在于我们常见的Widget及其属性中。好消息是Flutter Team在过去的更新迭代中已经剔除了非常多的不必要的类似上述的耗时操作 🐳 。所以当大家在自己的timeline中找到类似的耗时操作后,还是可以通过一些Widget的组件源码去分析找到出处,相较以前容易得多。

在上图的函数调用情况图中,短耗时的操作不代表其没有可能造成卡顿。有可能你找到了saveLayer的调用,但没有想象中的耗时。实际是因为:Skia的GPU的后端在接收到saveLayer的指令后会进行一个简单的预处理,就立即返回,造成不耗时的假象。但实际后续异步的过程中还会调用SkCanvas::Flush造成大量耗时。这里异步处理的目的是将多条预处理消息打包 📦 送至GPU进行绘制。

结束语

   有关Flutter中的界面绘制性能相关就介绍到这里了,希望这篇文章能给各位起到一定的帮助作用 🤪 ,后续会继续介绍Flutter相关的内存管理和其他相关知识,敬请期待 🧸 。
   参考:DevTools