为了账号安全,请及时绑定邮箱和手机立即绑定

程序员必备技能—debug [- Flutter -Dart 版-]

标签:
WebApp

https://img1.sycdn.imooc.com//5d78c9610001df0d06540368.jpg

相信自己,你90%的错误都可以通过debug自己解决。如果不是,那就尽快让自己成为90%的错误都可以通过debug解决的人。

猿非圣贤,孰能无bug。出现了bug第一件事是干嘛?  Google,百度,stakeover?
也许你该瞄一下被你冷落的日志,然后思考一下,无法解决时。深吸一口气,去debug!

一个bug 便是一场凶案,有着它特定的案发现场,别人很难掌握事情的来龙去脉
debug便是你且只有你,与凶手之间灵魂的碰撞,智商的博弈
当你抽丝剥茧,探寻蛛丝马迹,层层深入,最后手指前方,自信地说:“真想只有一个!”
凶手被抓,这时是何等快感,debug是你与程序的摩擦,是你与框架为数不多的交流与合作。
这时你不已再是一个API Caller,而是Program Coder,一位逻辑大侦探。
也许发现bug源头是个低级错误,你会拍着大腿,大骂自己4843,然后仰天长笑。
也许看似没有什么用处,但整个流程下来你完成了一次发现问题,解决问题的思维探索过程,
你做了一件和伽利略,牛顿,爱因斯坦一样的事:通过思考和实践解决问题
我们重要的是解决问题吗?更重要的是解决问题的过程和成长。

本文聚焦

void main() => runApp(Text("Debug")); 为什么报错!!! 复制代码


https://img1.sycdn.imooc.com//5d78c9740001c36803530288.jpg


这是笔者进入Flutter世界中的第一个疑问。Text也是Widget,为什么直接会崩溃?
从这个问题出发,一起来场debug之旅吧。注意:本文不注重讲知识点,重在debug的操作。
debug的重要性我就不说了,程序员不会debug,就像厨师不会用菜刀。
debug不止是寻找错误的,更重要的是辅助逻辑的分析,在分析中你也可以学到很多知识。


1. debug基本操作

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

https://img1.sycdn.imooc.com//5d78c99d0001b83312090163.jpg

https://img1.sycdn.imooc.com//5d78c9b10001736311020149.jpg

运行后会出现如下面板:你开始集结你的侦探团,面板的每个功能都是你的小伙伴。
他们有各自的特定和能力,会助你一起寻找凶手。


https://img1.sycdn.imooc.com//5d7bbadd0001df5606640210.jpg


1.1:三位侦查员小伙伴

分别介绍一下:下面是小折,小蓝和小红。


https://img1.sycdn.imooc.com//5d7bbaed0001bb1402780082.jpg

小折:不拘小节,统观全局,一行一行向下执行,遇见方法不会进入。口号:“大丈夫不拘小节”
小蓝:心思缜密,收放有度。如果其中有可执行单元 (非系统),则进入。口号:“走,进去探险吧。”
小红:细如丝缕,贯穿全局,即使是系统的源码,也会进入一探究竟。口号:“只要功夫深铁杵磨成针。”

所以这三人有各自的特点,侦查粒度依次更精细,可以根据情况酌情使用:
提个问题:当前状态点三位侦查员分别什么情况?

小折:哥不拘小节,执行下一行 : 控制台直接报错 小蓝:侦探怎么能像小折这么随意,遇到可执行单元,当然要去侦查一下,于是到达Text构造 小红:姐很忙,在非系统方法调试时,我和小蓝是一样的。小蓝进不去的,再来找我。 复制代码
构造函数相关:

这里要调试,当然选小蓝,然后会发现进入了Text的构造方法,其中的变量区显示着当前类的数据成员

https://img1.sycdn.imooc.com//5d7bbb020001511e06640276.jpg


再点一下小蓝可以发现它跳到了assert断言中,现在知道构造函数执行时会先执行冒号之后的语句。
而且每个字段之后都有提示信息,表明当前字段的值。


https://img1.sycdn.imooc.com//5d7bbb200001f8fb06690213.jpg


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



https://img1.sycdn.imooc.com//5d7bbb31000150b706550138.jpg由于Text继承自StatelessWidget,super方法调用父类。故进入:StatelessWidget构造



https://img1.sycdn.imooc.com//5d7bbb5e0001d88b06840129.jpg

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

https://img1.sycdn.imooc.com//5d7bbb7400017e2706730127.jpg

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

https://img1.sycdn.imooc.com//5d7bbb890001154e06760137.jpg

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

https://img1.sycdn.imooc.com//5d7bbba2000171fd06680159.jpg

由于mixin无构造函数,便到头了,于是方法入栈完毕,会依次弹栈:
Diagnosticable-->DiagnosticableTree-->Widget-->StatelessWidget-->Text-->runApp
当到Widget时,在其构造方法中对成员变量key进行复制,很显然由于我们Text没有传Key,所以为null:

https://img1.sycdn.imooc.com//5d7bbbbb00016d0006660148.jpg

接着便是一路弹栈,回到runApp,此时已经完成了对Text()对象的初始化

https://img1.sycdn.imooc.com//5d7bbbd10001c96006660105.jpg

这便是一个组件初始化的历程。接下来,小蓝会走到哪里呢?



1.2 runApp方法

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

https://img1.sycdn.imooc.com//5d7bbbf500019d3d06600152.jpg

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


https://img1.sycdn.imooc.com//5d7bbc0900019d9406620150.jpg

如果这时候你觉得这个方法不会有错,不想看了,毕竟是框架的初始化,不可能有错。你可以点四下小折,这样就该方法就会弹栈了。如果这个方法有100行呢,这是小折也感觉很累人。该怎么办?介绍一个新伙伴:小过 -- 将当前方法直接弹栈


https://img1.sycdn.imooc.com//5d7bbc1d00012ac406770162.jpg

就说明WidgetsBinding#ensureInitialized已经ok了,继续执行。


https://img1.sycdn.imooc.com//5d7bbc2b0001cfea06650176.jpg

用小蓝走几步,会到达attachRootWidget

https://img1.sycdn.imooc.com//5d7bbc5f00010f6b06580170.jpg

也许你那七秒钟的鱼一般的记忆会忘记attachRootWidget中传的参数是什么。
可以通过变量面板或者后面的提示来获取线索。

https://img1.sycdn.imooc.com//5d7bbc740001c29e06810259.jpg

这里通过RenderObjectToWidgetAdapter对象的attachToRenderTree方法为_renderViewElement进行赋值。这里小蓝下一步会走到哪?由于renderView是一个get方法,所以也是可执行单元,其中的三个入参的获取要早于RenderObjectToWidgetAdapter构造。 所以会先执行renderView方法。

https://img1.sycdn.imooc.com//5d7bbc8400019db206670132.jpg

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

https://img1.sycdn.imooc.com//5d7bbc94000186b906590215.jpg

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

https://img1.sycdn.imooc.com//5d7bbcab0001f54906590187.jpg

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

https://img1.sycdn.imooc.com//5d7bbcb70001306106550150.jpg

之后便是一批的父类构造进栈出栈,最后构造出RenderObjectToWidgetAdapter对象
挺无聊的,大丈夫不拘小节,小折来一下,该对象就构建完成了,


https://img1.sycdn.imooc.com//5d7bbccb00015cbc06570179.jpg

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

https://img1.sycdn.imooc.com//5d7bbcdc0001727106630159.jpg

在其中你可以输入表达式,下面会出现相应的结果


https://img1.sycdn.imooc.com//5d7bbcf10001b17906690197.jpg

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


https://img1.sycdn.imooc.com//5d7bbcff0001da4806640235.jpg

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


https://img1.sycdn.imooc.com//5d7bbd1400014fe606640221.jpg

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


https://img1.sycdn.imooc.com//5d7bbd240001fbf606640184.jpg

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


https://img1.sycdn.imooc.com//5d7bbd350001dec206590196.jpg

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


https://img1.sycdn.imooc.com//5d7bbd500001227106710129.jpg

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


1.3 元素的创建

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

https://img1.sycdn.imooc.com//5d7bbd620001533506670231.jpg

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

https://img1.sycdn.imooc.com//5d7bbd820001d5cb06670193.jpg

RenderObjectToWidgetAdapter继承树:     RenderObjectToWidgetAdapter-->RenderObjectWidget-->Widget        RenderObjectToWidgetElement元素继承树:     RenderObjectToWidgetElement-->RootRenderObjectElement-->RenderObjectElement-->Element 复制代码

https://img1.sycdn.imooc.com//5d7bbd950001b7a606550303.jpg

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


1.4 buildScope与元素装配

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

https://img1.sycdn.imooc.com//5d7bbdd80001762906620139.jpg

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

https://img1.sycdn.imooc.com//5d7bbde700013b7106680180.jpg

一路小折,到达第二参的回调处:

https://img1.sycdn.imooc.com//5d7bbdf20001237106630188.jpg

再进行小蓝,便会执行元素装载的方法,这也是Flutter中非常重要的一环。

https://img1.sycdn.imooc.com//5d7bbdfc0001db9006570195.jpg

首先装载会先调用父类的装载方法,最终追溯到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

https://img1.sycdn.imooc.com//5d7bbe0b00018b3906640237.jpg

而这个Widget何许人也,刚才Element笑纳的那个根Widget,即RenderObjectToWidgetAdapter
this是什么:RenderObjectElement,这也就是最顶层RenderObject被创建的时机。(画重点)

abstract class RenderObjectElement extends Element {.   RenderObjectElement(RenderObjectWidget widget) : super(widget);   @override   RenderObjectWidget get widget => super.widget; 复制代码

https://img1.sycdn.imooc.com//5d7bbe1c0001e4dd06710173.jpg

之后将元素关联到渲染对象上,并将自己标脏。更多的细节这里不再追,我有专文讲解
在父类的mount方法执行完后,会执行_rebuild方法。注意这时parent为null。

https://img1.sycdn.imooc.com//5d7bbe2a0001b3fc06630227.jpg

小蓝继续:你会走到updateChild,也就是将新旧孩子进行更新,敲黑板,划重点

RenderObjectToWidgetElement#mount     RenderObjectToWidgetElement#_rebuild         RenderObjectToWidgetElement#updateChild 复制代码

https://img1.sycdn.imooc.com//5d7bbe360001c98006580166.jpg

此时原来的孩子为null,新的孩子是我们传入的Text,接下来将何去何从?
发现没有符合if条件的,便传入的Text作为第一入参执行到最后inflateWidget

https://img1.sycdn.imooc.com//5d7bbe410001a66106680177.jpg

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


https://img1.sycdn.imooc.com//5d7bbe5100018f0f06570239.jpg

而这里的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


https://img1.sycdn.imooc.com//5d7bbe620001c67d06510255.jpg
ComponentElement#mount     ComponentElement#_firstBuild         ComponentElement#rebuild             ComponentElement#performRebuild 复制代码

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


https://img1.sycdn.imooc.com//5d7bbe6f0001a08506670214.jpg

问题来了,Text作为一个StatelessWidget它build的里到底做了什么? 答案是使用RichText进行内部组件的构造。你会发现,原来Text也并不想想象中的那么简单


https://img1.sycdn.imooc.com//5d7bbe7c0001abf306570219.jpg

在方法出栈是对build局部变量进行赋值,可以看出是一个RichText。

https://img1.sycdn.imooc.com//5d7bbe8b0001d73506640143.jpg

接下来又是一此updateChild,只不过主角不同了。

https://img1.sycdn.imooc.com//5d7bbe950001dbf706670190.jpg

这里的主角是RichText

https://img1.sycdn.imooc.com//5d7bbe9e0001552b06610151.jpg

Element#inflateWidget     RichText#createElement         MultiChildRenderObjectElement#createElement 复制代码

然后又会执行MultiChildRenderObjectElement的mount方法进行装载

https://img1.sycdn.imooc.com//5d7bbeac0001dde606640147.jpg

MultiChildRenderObjectElement#mount     RenderObjectElement#mount         Element#mount 复制代码

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


https://img1.sycdn.imooc.com//5d7bbebb000182db06620313.jpg

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

https://img1.sycdn.imooc.com//5d7bbec50001fcd206540247.jpg

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

https://img1.sycdn.imooc.com//5d7bbed1000133d606680225.jpg

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


2. 多断点的使用及其他

比如在inflateWidget这里再打个断点,运行时,首先会停留在第一个断点mian那里
但是之间的逻辑已经不需要再看了,使用不想一点点调试,那多断点就可以帮助你。

https://img1.sycdn.imooc.com//5d7bbede0001d5c106620193.jpg

点击这个,当前断点就会被放行,程序继续运行,当运行到下一个断点时就会停下,也就是inflateWidget处,这样就可以避免调试中间的流程。当你在调试时,可以先选一些肯出错的地方,打上断点,然后再去调试。这样能更迅速的定位到bug所在。


https://img1.sycdn.imooc.com//5d7bbeee00012fdd06770159.jpg

也许你会怕断点太多怎么办?这里可以对断点进行查看和修改。


https://img1.sycdn.imooc.com//5d7bbf050001f8bd06690231.jpg

Run to Cursor可以让程序运行到指定光标处,注意它碰到其他断点会先停留在断点处


https://img1.sycdn.imooc.com//5d7bbf140001565406640254.jpg

最后说一下变量观察和循环调试:

如果变量过多,可以通过Watcher进行单独观察,点加号,输入变量名即可


https://img1.sycdn.imooc.com//5d7bbf230001373606650142.jpg

如果有一千万次循环,一步一步还不得地老天荒,这时候循环调试可以帮到你
你可以指定一个条件,那么下次循环就会变成此条件。


https://img1.sycdn.imooc.com//5d7bbf400001ccca06630487.jpg

好了就说这么多。



点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
20
获赞与收藏
56

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消