猿非圣贤,孰能无 bug。出现了 bug 第一件事是干嘛? Google,百度,Stack Overflow?
也许你该瞄一下被你冷落的日志,然后思考一下,无法解决时。深吸一口气,去 debug!
一个 bug 便是一场凶案,有着它特定的案发现场,别人很难掌握事情的来龙去脉 debug 便是你且只有你,与凶手之间灵魂的碰撞,智商的博弈。当你抽丝剥茧,探寻蛛丝马迹,层层深入,最后手指前方,自信地说: "真相只有一个!" 凶手被抓,这时是何等快感。
本文聚焦
void main() => runApp(Text("Debug")); 为什么报错!!!
debug 基本操作
三位侦查员小伙伴
小折: 不拘小节,统观全局,一行一行向下执行,遇见方法不会进入。
口号: "大丈夫不拘小节"
小蓝: 心思缜密,收放有度。如果其中有可执行单元 (非系统),则进入。
口号: "走,进去探险吧"
小红: 细如丝缕,贯穿全局,即使是系统的源码,也会进入一探究竟。
口号: "只要功夫深铁杵磨成针"
小折:哥不拘小节,执行下一行 : 控制台直接报错
小蓝:侦探怎么能像小折这么随意,遇到可执行单元,当然要去侦查一下,于是到达Text构造
小红:姐很忙,在非系统方法调试时,我和小蓝是一样的。小蓝进不去的,再来找我。
构造函数相关
点小蓝,到达执行到 super(key:key),提问: "再点小蓝会到哪?"
同理: StatelessWidget 也先执行 super(key:key),镜头转到: Widget 构造
接下来小蓝往哪走? 要知道,一个类的初始化,首先要执行其父类的构造函数。这里 Widget 继承自 DiagnosticableTree,必然会先执行 DiagnosticableTree 的构造方法
由于 mixin 无构造函数,便到头了,于是方法入栈完毕,会依次弹栈: Diagnosticable-->DiagnosticableTree-->Widget-->StatelessWidget-->Text-->runApp
当到 Widget 时,在其构造方法中对成员变量 key 进行复制,很显然由于我们 Text 没有传 Key,所以为 null:
runApp方法
接下来很显然小蓝要走入 WidgetsBinding 的 ensureInitialized 方法
这时如果你好奇 RenderView 是什么,然后点了进去: 发现它是一个 RenderObject
之后便会进入 RenderObjectToWidgetAdapter 的构造方法,入参是什么,还用我说么?
只要是可以运算的,都能在这里运算查看结果。发现 renderViewElement 是一个 null
再点击小蓝时,毫无疑问,走到 lockState 方法中,而入参便是上面那一坨。小折走几步发现到了 callback();,之后小蓝会到哪?
这里便进入了createElement方法,也就是元素的创建实机。
元素的创建
这里入参 widget 是什么,很显然,什么传入的是 this,表明是 RenderObjectToWidgetAdapter 对象。RenderObjectToWidgetAdapter 是什么,是包含着我们的 Text 的一个 Widget。
RenderObjectToWidgetAdapter继承树:
RenderObjectToWidgetAdapter-->RenderObjectWidget-->Widget
RenderObjectToWidgetElement元素继承树:
RenderObjectToWidgetElement-->RootRenderObjectElement-->RenderObjectElement-->Element
这样 RenderObjectToWidgetAdapter(Widget) 就被 RenderObjectToWidgetElement(Element) 纳为己有,元素也被成功创建,小蓝继续来到这里刚才创建元素的方法中。走几步便到达 owner.buildScope 方法,将刚才的元素传入,并在回调中执行元素的装载方法 (mount) 这是顶层元素的装配点,记住它被触发的时机,划重点,要考的。
这里提问: 小蓝在此时会走到哪? 我好像听到有人说是先执行第二个入参里的方法,因为它可执行。答案是进入 buildScope,因为第二参只是一个函数类型的入参,并没有被触发。于是到达了 buildScope,这个Flutter框架核心环节之一:
RenderObjectToWidgetElement# mount
RootRenderObjectElement# mount
RenderObjectElement# mount
Element# mount
Element# mount 做的最重要的一件事就是将 _parent 和 _slot 通过入参进行初始化。
---->[Element
void mount(Element parent, dynamic newSlot) {
//略...
_parent = parent;
_slot = newSlot;
//略...
}
abstract class RenderObjectElement extends Element {.
RenderObjectElement(RenderObjectWidget widget) : super(widget);
RenderObjectWidget get widget => super.widget;
RenderObjectToWidgetElement#mount
RenderObjectToWidgetElement#_rebuild
RenderObjectToWidgetElement#updateChild
在 inflateWidget 中可以发现一个惊天秘密,也就是 Widget 触发 createElement 的时机。
abstract class StatelessWidget extends Widget {
StatelessElement createElement() => StatelessElement(this);
ComponentElement#mount
ComponentElement#_firstBuild
ComponentElement#rebuild
ComponentElement#performRebuild
在 performRebuild 中你会发现 build 的调用时机。
问题来了,Text 作为一个 StatelessWidget 它 build 的里到底做了什么?
答案是使用 RichText 进行内部组件的构造。你会发现,原来 Text 也并不想想象中的那么简单。
Element#inflateWidget
RichText#createElement
MultiChildRenderObjectElement#createElement
MultiChildRenderObjectElement#mount
RenderObjectElement#mount
Element#mount
也许你并不知道 RichText 是通过 RenderParagraph 进行创建 RenderObject 的,而这一开始的错误便是来源于这个断言。
这样就也找到了异常所在。
这里为了带大家多了解一些知识,所以跳的比较细,其实很多地方可以用小折直接过。不过小蓝可以帮助你分析程序的运行逻辑,对你把控整个框架有很大的帮助。如果是学习可以用小蓝,更加细致。
多断点的使用及其他
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"