Flutter 技能篇: debug 看程序启动 | 开发者说·DTalk

2020 年 4 月 23 日 谷歌开发者

本文原作者:张风捷特烈,原文发布于掘金:

https://juejin.im/post/5d735052f265da03ca119183


猿非圣贤,孰能无 bug。出现了 bug 第一件事是干嘛? Google,百度,Stack Overflow?


也许你该瞄一下被你冷落的日志,然后思考一下,无法解决时。深吸一口气,去 debug!


一个 bug 便是一场凶案,有着它特定的案发现场,别人很难掌握事情的来龙去脉 debug 便是你且只有你,与凶手之间灵魂的碰撞,智商的博弈。当你抽丝剥茧,探寻蛛丝马迹,层层深入,最后手指前方,自信地说: "真相只有一个!" 凶手被抓,这时是何等快感。


debug 是你与程序的摩擦,是你与框架为数不多的交流与合作。这时你不已再是一个 API Caller,而是 Program Coder,一位逻辑大侦探。也许发现 bug 源头是个低级错误,你会拍着大腿,大骂自己 4843,然后仰天长笑。也许看似没有什么用处,但整个流程下来你完成了一次发现问题,解决问题的思维探索过程,你做了一件和伽利略、牛顿、爱因斯坦一样的事: 通过思考和实践解决问题。

我们重要的是解决问题吗? 更重要的是解决问题的过程和成长。



本文聚焦


void main() => runApp(Text("Debug")); 为什么报错!!!
这是笔者进入 Flutter 世界中的第一个疑问。Text 也是 Widget,为什么直接会崩溃? 从这个问题出发,一起来场 debug 之旅吧。(注意: 本文不注重讲知识点,重在 debug 的操作)

debug 的重要性我就不说了,程序员不会 debug,就像厨师不会用菜刀。debug 不止是寻找错误的,更重要的是辅助逻辑的分析,在分析中你也可以学到很多知识。



 debug 基本操作


首先打断点,在左侧点击,会出现一个小红点,当 debug 模式运行时, 程序会停在这里,也就是凶案已经发生,你让整个世界暂停,以便你这个大侦探进行调查。

运行后会出现如下面板: 你开始集结你的侦探团,面板的每个功能都是你的小伙伴。

他们有各自的特定和能力,会助你一起寻找凶手。

三位侦查员小伙伴

分别介绍一下: 下面是小折,小蓝和小红。
  • 小折: 不拘小节,统观全局,一行一行向下执行,遇见方法不会进入。

    口号: "大丈夫不拘小节"

  • 小蓝: 心思缜密,收放有度。如果其中有可执行单元 (非系统),则进入。

    口号: "走,进去探险吧"

  • 小红: 细如丝缕,贯穿全局,即使是系统的源码,也会进入一探究竟。

    口号: "只要功夫深铁杵磨成针"


所以这三人有各自的特点,侦查粒度依次更精细, 可以根据情况酌情使用。提个问题: 当前状态点三位侦查员分别什么情况?
小折:哥不拘小节,执行下一行 : 控制台直接报错小蓝:侦探怎么能像小折这么随意,遇到可执行单元,当然要去侦查一下,于是到达Text构造小红:姐很忙,在非系统方法调试时,我和小蓝是一样的。小蓝进不去的,再来找我。

构造函数相关

这里要调试,当然选小蓝,然后会发现进入了 Text 的构造方法,其中的变量区显示着当前类的数据成员。
再点一下小蓝可以发现它跳到了 assert 断言中, 现在知道构造函数执行时会先执行冒号之后的语句。而且每个字段之后都有提示信息,表明当前字段的值。

点小蓝,到达执行到 super(key:key),提问: "再点小蓝会到哪?"

由于 Text 继承自 StatelessWidget,super 方法调用父类。故进入: StatelessWidget 构造

同理: StatelessWidget 也先执行 super(key:key),镜头转到: Widget 构造

接下来小蓝往哪走? 要知道,一个类的初始化,首先要执行其父类的构造函数。这里 Widget 继承自 DiagnosticableTree,必然会先执行 DiagnosticableTree 的构造方法

同理: DiagnosticableTree 继承自 Diagnosticable, 要执行其父类的构造函数。这里进栈的顺序是: runApp-->Text-->StatelessWidget-->Widget-->DiagnosticableTree-->Diagnosticable

由于 mixin 无构造函数,便到头了,于是方法入栈完毕,会依次弹栈: Diagnosticable-->DiagnosticableTree-->Widget-->StatelessWidget-->Text-->runApp


当到 Widget 时,在其构造方法中对成员变量 key 进行复制,很显然由于我们 Text 没有传 Key,所以为 null:

接着便是一路弹栈,回到 runApp,此时已经完成了对 Text() 对象的初始化
这便是一个组件初始化的历程。接下来,小蓝会走到哪里呢?

runApp方法

由于 runApp 是可执行方法,小蓝会进入 runApp 方法,将刚才的 Text 对象作为入参:

接下来很显然小蓝要走入 WidgetsBinding 的 ensureInitialized 方法

如果这时候你觉得这个方法不会有错,不想看了,毕竟是框架的初始化,不可能有错。你可以点四下小折,这样就该方法就会弹栈了。如果这个方法有 100 行呢,这是小折也感觉很累人。该怎么办? 介绍一个新伙伴:  小过 -- 将当前方法直接弹栈
就说明 WidgetsBinding#ensureInitialized 已经 ok 了,继续执行。
用小蓝走几步,会到达 attachRootWidget
也许你那七秒钟的鱼一般的记忆会忘记 attachRootWidget 中传的参 数是什么,可以通过变量面板或者后面的提示来获取线索。
这里通过 RenderObjectToWidgetAdapter 对象的 attachToRenderTree 方法为 _renderViewElement 进行赋值。这里小蓝下一步会走到哪? 由于 renderView 是一个 get 方法,所以也是可执行单元,其中的三个入参的获取要早于 RenderObjectToWidgetAdapter 构造,所以会先执行 renderView 方法。

这时如果你好奇 RenderView 是什么,然后点了进去: 发现它是一个 RenderObject

这时你想回到刚才程序运行的地方, 但是七秒钟记忆的你忘记了怎么办? 放心,有一个小伙伴帮你看着呢,他就是小前----回到刚才程序运行处,由于他的看守,所以你可以肆无忌惮地乱跑,点击一下他,就能回到刚才程序运行的地方。

之后便会进入 RenderObjectToWidgetAdapter 的构造方法,入参是什么,还用我说么?

之后便是一批的父类构造进栈出栈,最后构造出 RenderObjectToWidgetAdapter 对象。

挺无聊的,大丈夫不拘小节,小折来一下,该对象就构建完成了。

然后小蓝会该对象的走 attachToRenderTree 方法,等一下,这两个入参是什么? 当你对入参有所疑惑,或想要查看当前表达式的的结果,那么另一个小伙伴就会很有用,她就是依依--计算表达式。
在其中你可以输入表达式,下面会出现相应的结果

只要是可以运算的,都能在这里运算查看结果。发现 renderViewElement 是一个 null

继续小蓝,renderViewElement 和 buildOwner 都是一个 get 函数, 所以都会进入。如果一直小蓝,获取的个个细节都会被一点点走过,如果不想看这么多,小折或小过就行了。这时候会到达 attachToRenderTree,这里提一下,下面的 Frames 里可以看到当前执行处所在的位置,让你不至于连在哪都不知道。

刚才已经看了,这个 element 为 null,所以会走 owner.lockState 方法,注意这里的参数是一个函数。

再点击小蓝时,毫无疑问,走到 lockState 方法中,而入参便是上面那一坨。小折走几步发现到了 callback();,之后小蓝会到哪?

你猜对了,是执行刚才的入参函数,(敲黑板) 注意了,要考的

这里便进入了createElement方法,也就是元素的创建实机。


元素的创建

在 RenderObjectToWidgetAdapter 中通过 RenderObjectToWidgetElement 完成创建元素。

这里入参 widget 是什么,很显然,什么传入的是 this,表明是 RenderObjectToWidgetAdapter 对象。RenderObjectToWidgetAdapter 是什么,是包含着我们的 Text 的一个 Widget。

RenderObjectToWidgetAdapter继承树:    RenderObjectToWidgetAdapter-->RenderObjectWidget-->Widget
RenderObjectToWidgetElement元素继承树: RenderObjectToWidgetElement-->RootRenderObjectElement-->RenderObjectElement-->Element

这里 RenderObjectToWidgetElement 将该 Widget 一路供奉给父类构造,并在 Element 中被笑纳。可以看出在 RenderObjectToWidgetElement 元素中获取 get widget 是通过 super 拿来的 Widget。

buildScope 与元素装配

这样 RenderObjectToWidgetAdapter(Widget) 就被 RenderObjectToWidgetElement(Element) 纳为己有,元素也被成功创建,小蓝继续来到这里刚才创建元素的方法中。走几步便到达 owner.buildScope 方法,将刚才的元素传入,并在回调中执行元素的装载方法 (mount) 这是顶层元素的装配点,记住它被触发的时机,划重点,要考的。

这里提问: 小蓝在此时会走到哪? 我好像听到有人说是先执行第二个入参里的方法,因为它可执行。答案是进入 buildScope,因为第二参只是一个函数类型的入参,并没有被触发。于是到达了 buildScope,这个Flutter框架核心环节之一:

一路小折,到达第二参的回调处:
再进行小蓝,便会执行元素装载的方法,这也是 Flutter 中非常重要的一环。
首先装载会先调用父类的装载方法,最终追溯到 Element# mount。
RenderObjectToWidgetElement# mount    RootRenderObjectElement# mount        RenderObjectElement# mount                    Element# mount


Element# mount 做的最重要的一件事就是将 _parent 和 _slot 通过入参进行初始化。

---->[Element# mount]---void mount(Element parent, dynamic newSlot) {  //略...  _parent = parent;  _slot = newSlot;  //略...}

然后一路弹栈: RenderObjectElement# mount 中通过 widget 来创建 RenderObject。
而这个 Widget 何许人也,刚才 Element 笑纳的那个根 Widget,即 RenderObjectToWidgetAdapter
this 是什么: RenderObjectElement,这也就是最顶层 RenderObject 被创建的时机。(画重点)
abstract class RenderObjectElement extends Element {.  RenderObjectElement(RenderObjectWidget widget) : super(widget);
@override RenderObjectWidget get widget => super.widget;

之后将元素关联到渲染对象上,并将自己标脏。更多的细节这里不再追,我有专文讲解
在父类的 mount 方法执行完后,会执行 _rebuild 方法。注意这时 parent为null。
小蓝继续: 你会走到 updateChild,也就是将新旧孩子进行更新,敲黑板,划重点。
RenderObjectToWidgetElement#mount    RenderObjectToWidgetElement#_rebuild        RenderObjectToWidgetElement#updateChild

此时原来的孩子为 null,新的孩子是我们传入的 Text,接下来将何去何从?

发现没有符合 if 条件的,便传入的 Text 作为第一入参执行到最后 inflateWidget。

在 inflateWidget 中可以发现一个惊天秘密,也就是 Widget 触发 createElement 的时机。

而这里的 newWidget 是 Text,那现在有一个值得深思的问题: Text 的 createElement 是如何实现的? 由于 Text 是一个 StatelessWidget,所以必然是走 StatelessWidget 的 createElement,返回一个 StatelessElement 对象,并将该 Widget 笑纳。
abstract class StatelessWidget extends Widget {  @override  StatelessElement createElement() => StatelessElement(this);

然后便会指 向该元素的 mount 方法,该元素是谁? StatelessElement
由于 StatelessElement 未复写 mount 方法,会直接走父类: ComponentElement#mount。
ComponentElement#mount    ComponentElement#_firstBuild        ComponentElement#rebuild            ComponentElement#performRebuild


在 performRebuild 中你会发现 build 的调用时机。

问题来了,Text 作为一个 StatelessWidget 它 build 的里到底做了什么?


答案是使用 RichText 进行内部组件的构造。你会发现,原来 Text 也并不想想象中的那么简单。

在方法出栈是对 build 局部变量进行赋值,可以看出是一个 RichText。
接下来又是一此 updateChild,只不过主角不同了。
这里的主角是 RichText。
Element#inflateWidget    RichText#createElement        MultiChildRenderObjectElement#createElement

然后又会执行 MultiChildRenderObjectElement 的 mount 方法进行装载。
MultiChildRenderObjectElement#mount    RenderObjectElement#mount        Element#mount

你会发现有执行到了 RenderObjectEle ment#mount,只是这时的 parent 是我们传入的 Text,因为这时对 Text 的自件 RichText 进行了装载。

也许你并不知道 RichText 是通过 RenderParagraph 进行创建 RenderObject 的,而这一开始的错误便是来源于这个断言。

这样就也找到了异常所在。

这里为了带大家多了解一些知识,所以跳的比较细,其实很多地方可以用小折直接过。不过小蓝可以帮助你分析程序的运行逻辑,对你把控整个框架有很大的帮助。如果是学习可以用小蓝,更加细致。



多断点的使用及其他


比如在 inflateWidget 这里再打个断点,运行时,首先会停留在第一个断点 mian 那里。

但是之间的逻辑已经不需要再看了,使用不想一点点调试,那多断点就可以帮助你。
点击这个,当前断点就会被放行,程序继续运行,当运行到下一个断点时就会停下,也就是 inflateWidget 处,这样就可以避免调试中间的流程。当你在调试时,可以先选一些肯出错的地方,打上断点,然后再去调试。这样能更迅速的定位到 bug 所在。
也许你会怕断点太多怎么办? 这里可以对断点进行查看和修改。
Run to Cursor 可以让程序运行到指定光标处,注意它碰到其他断点会先停留在断点处。
最后说一下变量观察和循环调试: 如果变量过多,可以通过Watcher进行单独观察,点加号,输入变量名即可。
如果有一千万次循环,一步一步还不得地老天荒,这时候循环调试可以帮到你。你可以指定一个条件,那么下次循环就会变成此条件。

好了就说这么多。



长按右侧二维码

查看更多开发者精彩分享




"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。



 

 点击屏末 |  | 即刻报名参与 "开发者说·DTalk" 

 



登录查看更多
0

相关内容

程序猿的天敌 有时是一个不能碰的magic
【2020新书】实战R语言4,323页pdf
专知会员服务
100+阅读 · 2020年7月1日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
225+阅读 · 2020年3月22日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
msf实现linux shell反弹
黑白之道
49+阅读 · 2019年8月16日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
Python | Jupyter导出PDF,自定义脚本告别G安装包
程序人生
7+阅读 · 2018年7月17日
Python 杠上 Java、C/C++,赢面有几成?
CSDN
6+阅读 · 2018年4月12日
为什么你应该学 Python ?
计算机与网络安全
4+阅读 · 2018年3月24日
十五条有用的Golang编程经验
CSDN大数据
5+阅读 · 2017年8月7日
Arxiv
26+阅读 · 2020年2月21日
Deflecting Adversarial Attacks
Arxiv
8+阅读 · 2020年2月18日
EfficientDet: Scalable and Efficient Object Detection
Arxiv
6+阅读 · 2019年11月20日
Deep Learning for Deepfakes Creation and Detection
Arxiv
6+阅读 · 2019年9月25日
Arxiv
5+阅读 · 2019年4月8日
Arxiv
3+阅读 · 2018年6月24日
VIP会员
相关VIP内容
【2020新书】实战R语言4,323页pdf
专知会员服务
100+阅读 · 2020年7月1日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
225+阅读 · 2020年3月22日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
相关资讯
msf实现linux shell反弹
黑白之道
49+阅读 · 2019年8月16日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
Python | Jupyter导出PDF,自定义脚本告别G安装包
程序人生
7+阅读 · 2018年7月17日
Python 杠上 Java、C/C++,赢面有几成?
CSDN
6+阅读 · 2018年4月12日
为什么你应该学 Python ?
计算机与网络安全
4+阅读 · 2018年3月24日
十五条有用的Golang编程经验
CSDN大数据
5+阅读 · 2017年8月7日
相关论文
Top
微信扫码咨询专知VIP会员