You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

206 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 36 | 如何通过工具链优化开发调试效率?
你好我是陈航。今天我们来聊聊如何调试Flutter App。
软件开发通常是一个不断迭代、螺旋式上升的过程。在迭代的过程中我们不可避免地会经常与Bug打交道特别是在多人协作的项目中我们不仅要修复自己的Bug有时还需要帮别人解决Bug。
而修复Bug的过程不仅能帮我们排除代码中的隐患也能帮助我们更快地上手项目。因此掌握好调试这门技能就显得尤为重要了。
在Flutter中调试代码主要分为输出日志、断点调试和布局调试3类。所以在今天这篇文章中我将会围绕这3个主题为你详细介绍Flutter应用的代码调试。
我们先来看看,如何通过输出日志调试应用代码吧。
## 输出日志
为了便于跟踪和记录应用的运行情况我们在开发时通常会在一些关键步骤输出日志Log即使用print函数在控制台打印出相关的上下文信息。通过这些信息我们可以定位代码中可能出现的问题。
在前面的很多篇文章里我们都大量使用了print函数来输出应用执行过程中的信息。不过由于涉及I/O操作使用print来打印信息会消耗较多的系统资源。同时这些输出数据很可能会暴露App的执行细节所以我们需要在发布正式版时屏蔽掉这些输出。
说到操作方法你想到的可能是在发布版本前先注释掉所有的print语句等以后需要调试时再取消这些注释。但这种方法无疑是非常无聊且耗时的。那么Flutter给我们提供了什么更好的方式吗
为了根据不同的运行环境来开启日志调试功能我们可以使用Flutter提供的debugPrint来代替print。**debugPrint函数同样会将消息打印至控制台但与print不同的是它提供了定制打印的能力。**也就是说我们可以向debugPrint函数赋值一个函数声明来自定义打印行为。
比如在下面的代码中我们将debugPrint函数定义为一个空函数体这样就可以实现一键取消打印的功能了
```
debugPrint = (String message, {int wrapWidth}) {};//空实现
```
在Flutter 中我们可以使用不同的main文件来表示不同环境下的入口。比如在第34篇文章“[如何理解Flutter的编译模式](https://time.geekbang.org/column/article/135865)”中我们就分别用main.dart与main-dev.dart实现了生产环境与开发环境的分离。同样我们可以通过main.dart与main-dev.dart去分别定义生产环境与开发环境不同的打印日志行为。
在下面的例子中我们将生产环境的debugPrint定义为空实现将开发环境的debugPrint定义为同步打印数据
```
//main.dart
void main() {
// 将debugPrint指定为空的执行体, 所以它什么也不做
debugPrint = (String message, {int wrapWidth}) {};
runApp(MyApp());
}
//main-dev.dart
void main() async {
// 将debugPrint指定为同步打印数据
debugPrint = (String message, {int wrapWidth}) => debugPrintSynchronously(message, wrapWidth: wrapWidth);
runApp(MyApp());
}
```
可以看到在代码实现上我们只要将应用内所有的print都替换成debugPrint就可以满足开发环境下打日志的需求也可以保证生产环境下应用的执行信息不会被意外打印。
## 断点调试
输出日志固然方便,但如果要想获取更为详细,或是粒度更细的上下文信息,静态调试的方式非常不方便。这时,我们需要更为灵活的动态调试方法,即断点调试。断点调试可以让代码在目标语句上暂停,让程序逐条执行后续的代码语句,来帮助我们实时关注代码执行上下文中所有变量值的详细变化过程。
Android Studio提供了断点调试的功能调试Flutter应用与调试原生Android代码的方法完全一样具体可以分为三步即**标记断点、调试应用、查看信息**。
接下来我们以Flutter默认的计数器应用模板为例观察代码中\_counter值的变化体会断点调试的全过程。
**首先是标记断点。**既然我们要观察\_counter值的变化因此在界面上展示最新的\_counter值时添加断点去观察其数值变化是最理想的。因此我们在行号右侧点击鼠标可以把断点加载到初始化Text控件所示的位置。
在下图的例子中,我们为了观察\_counter在等于20的时候是否正常还特意设置了一个条件断点\_counter==20这样调试器就只会在第20次点击计数器按钮时暂停下来
![](https://static001.geekbang.org/resource/image/dc/4f/dcccfaa6fcfb3bd2dc5be627129a244f.png)
图1 标记断点
添加断点后,对应的行号将会出现圆形的断点标记,并高亮显示整行代码。到此,断点就添加好了。当然,我们还可以同时添加多个断点,以便更好地观察代码的执行过程。
**接下来则是调试应用了。**和之前通过点击run按钮的运行方式不同这一次我们需要点击工具栏上的虫子图标以调试模式启动App如下图所示
![](https://static001.geekbang.org/resource/image/d9/9c/d944045bd22008a14e6f027015cd5c9c.png)
图2 调试App
等调试器初始化好后,我们的程序就启动了。由于我们的断点设置在了\_counter为20时因此在第20次点击了“+”按钮后代码运行到了断点位置自动进入了Debug视图模式。
![](https://static001.geekbang.org/resource/image/95/09/959408a818e9e978c6ff830f2e400609.png)
图3 Debug视图模式
如图所示我把Debug视图模式划分为4个区域即A区控制调试工具、B区步进调试工具、C区帧调试窗口、D区变量查看窗口。
**A区的按钮**,主要用来控制调试的执行情况:
![](https://static001.geekbang.org/resource/image/ce/4b/ceaed745cf0deceef2fd0dbcd680dc4b.png)
图4 A区按钮
* 比如,我们可以点击继续执行按钮来让程序继续运行、点击终止执行按钮来让程序终止运行、点击重新执行按钮来让程序重新启动,或是在程序正常执行时,点击暂停执行按钮按钮来让程序暂停运行。
* 又比如,我们可以点击编辑断点按钮来编辑断点信息,或是点击禁用断点按钮来取消断点。
**B区的按钮**,主要用来控制断点的步进情况:
![](https://static001.geekbang.org/resource/image/56/9c/56a8ba3a79d100e03a28001b3b5dad9c.png)
图5 B区按钮
* 比如,我们可以点击单步跳过按钮来让程序单步执行(但不会进入方法体内部)、点击单步进入或强制单步进入按钮让程序逐条语句执行,甚至还可以点击运行到光标处按钮让程序执行到在光标处(相当于新建临时断点)。
* 比如,当我们认为断点所在的方法体已经无需执行时,则可以点击单步跳出按钮让程序立刻执行完当前进入的方法,从而返回方法调用处的下一行。
* 又比如,我们可以点击表达式计算按钮来通过赋值或表达式方式修改任意变量的值。如下图所示,我们通过输入表达式\_counter+=100将计数器更新为120
![](https://static001.geekbang.org/resource/image/6e/2f/6ea7684f8ce9dabdd1d42e7f38b38a2f.png)
图6 Evaluate计算表达式
**C区**用来指示当前断点所包含的函数执行堆栈,**D区**则是其堆栈中的函数帧所对应的变量。
在这个例子中,我们的断点是在\_MyHomePageState类中的build方法设置的因此D区显示的也是build方法上下文所包含的变量信息比如\_counter、\_widget、this、\_element等。如果我们想切换到\_MyHomePageState的build方法执行堆栈中的其他函数比如StatefulElement.build查看相关上下文的变量信息时只需要在C区中点击对应的方法名即可。
![](https://static001.geekbang.org/resource/image/55/8f/5552132471184515ef62d3493492368f.png)
图7 切换函数执行堆栈
可以看到Android Studio提供的Flutter调试能力很丰富我们可以通过这些基本步骤的组合更为灵活地调整追踪步长观察程序的执行情况揪出代码中的Bug。
## 布局调试
通过断点调试我们在Android Studio的调试面板中可以随时查看执行上下文有关的变量的值根据逻辑来做进一步的判断确定跟踪执行的步骤。不过在更多时候我们使用Flutter的目的是实现视觉功能而视觉功能的调试是无法简单地通过Debug视图模式面板来搞定的。
在上一篇文章中我们通过Flutter提供的热重载机制已经极大地缩短了从代码编写到界面运行所耗费的时间可以更快地发现代码与目标界面的明显问题但**如果想要更快地发现界面中更为细小的问题比如对齐、边距等则需要使用Debug Painting这个界面调试工具**。
Debug Painting能够以辅助线的方式清晰展示每个控件元素的布局边界因此我们可以根据辅助线快速找出布局出问题的地方。而Debug Painting的开启也比较简单只需要将debugPaintSizeEnabled变量置为true即可。如下所示我们在main函数中开启了Debug Painting调试开关
```
import 'package:flutter/rendering.dart';
void main() {
debugPaintSizeEnabled = true; //打开Debug Painting调试开关
runApp(new MyApp());
}
```
运行代码后App在iPhone X中的执行效果如下
![](https://static001.geekbang.org/resource/image/4a/74/4ae96d4e0bb7dc868ca92753c9bb1574.png)
图8 Debug Painting运行效果
可以看到,计数器示例中的每个控件元素都已经被标尺辅助线包围了。
辅助线提供了基本的Widget可视化能力。通过辅助线我们能够感知界面中是否存在对齐或边距的问题但却没有办法获取到布局信息比如Widget距离父视图的边距信息、Widget宽高尺寸信息等。
**如果我们想要获取到Widget的可视化信息比如布局信息、渲染信息等去解决渲染问题就需要使用更强大的Flutter Inspector了。**Flutter Inspector对控件布局详细数据提供了一种强大的可视化手段来帮助我们诊断布局问题。
为了使用Flutter Inspector我们需要回到Android Studio通过工具栏上的“Open DevTools”按钮启动Flutter Inspector
![](https://static001.geekbang.org/resource/image/ff/65/ff54c2a1883bb01f9db2b0f64bf75965.png)
图9 Flutter Inspector启动按钮
随后Android Studio会打开浏览器将计数器示例中的Widget树结构展示在面板中。可以看到Flutter Inspector所展示的Widget树结构与代码中实现的Widget层次是一一对应的。
![](https://static001.geekbang.org/resource/image/49/56/4939857499b003b5018737c965c30f56.png)
图10 Flutter Inspector示意图
我们的App运行在iPhone X之上其分辨率为375\*812。接下来我们以Column组件的布局信息为例通过确认其水平方向为居中布局、垂直方向为充满父Widget剩余空间的过程来说明**Flutter Inspector的具体用法**。
为了确认Column在垂直方向是充满其父Widget剩余空间的我们首先需要确定其父Widget在垂直方向上的另一个子Widget即AppBar的信息。我们点击Flutter Inspector面板左侧中的AppBar控件右侧对应显示了它的具体视觉信息。
可以看到AppBar控件距离左边距为0上边距也为0宽为375高为100
![](https://static001.geekbang.org/resource/image/10/d7/1000678ddbe58f74aa9c2da1ed20a1d7.png)
图11 Flutter Inspector之AppBar
然后我们将Flutter Inspector面板左侧选择的控件更新为Column右侧也更新了它的具体视觉信息比如排版方向、对齐模式、渲染信息以及它的两个子Widget-Text。
可以看到Column控件的距离左边距为38.5上边距为0宽为298高为712
![](https://static001.geekbang.org/resource/image/84/d9/84b689d4e0c98f07831810cb346278d9.png)
图12 Flutter Inspector之Columnn
通过上面的数据我们可以得出:
* Column的右边距=父Widget宽度即iPhone X宽度375-Column左边距即38.5- Column宽即298=38.5即左右边距相等因此Column是水平方向居中的
* Column的高度=父Widget的高度即iPhone X高度812- AppBar上边距即0- AppBar高度即100 - Column上边距即0= 712.0即Column在垂直方向上完全填满了父Widget除去AppBar之后的剩余空间。
因此Column的布局行为是完全符合预期的。
## 总结
好了,今天的分享就到这里,我们总结一下今天的主要内容吧。
首先我带你学习了如何实现定制日志的输出能力。Flutter提供了debugPrint函数这是一个可以被覆盖的打印函数。我们可以分别定义生产环境与开发环境的日志输出行为来满足开发期打日志需求的同时保证发布期日志执行信息不会被意外打印。
然后我与你介绍了Android Studio提供的Flutter调试功能并通过观察计数器工程的计数器变量为例与你讲述了具体的调试方法。
最后我们一起学习了Flutter的布局调试能力即通过Debug Paiting来定义辅助线以及通过Flutter Inspector这种可视化手段来更为准确地诊断布局问题。
写代码不可避免会出现Bug出现时就需要Debug调试。调试代码本质上就是一个不断收敛问题发生范围的过程因此排查问题的一个最基本思路就是二分法。
所谓二分调试法是指通过某种稳定复现的特征比如Crash、某个变量的值、是否出现某个现象等任何明显的迹象加上一个能把问题出现的范围划分为两半的手段比如断点、assert、日志等两者结合反复迭代不断将问题可能出现的范围一分为二比如能判断出引发问题的代码出现在断点之前等。通过二分法我们可以快速缩小问题范围这样一来调试的效率也就上去了。
## 思考题
最后,我给你留下一道思考题吧。
请将debugPrint在生产环境下的打印日志行为更改为写日志文件。其中日志文件一共5个0-4每个日志文件不能超过2MB但可以循环写。如果日志文件已满则循环至下一个日志文件清空后重新写入。
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。