相信本身,你90%的錯誤均可以經過debug本身解決。若是不是,那就儘快讓本身成爲90%的錯誤均可以經過debug解決的人。
程序員
猿非聖賢,孰能無bug。出現了bug第一件事是幹嗎? Google,百度,stakeover?
也許你該瞄一下被你冷落的日誌,而後思考一下,沒法解決時。深吸一口氣,去debug!bash
一個bug 即是一場兇案,有着它特定的案發現場,別人很難掌握事情的前因後果
debug即是你且只有你,與兇手之間靈魂的碰撞,智商的博弈
當你抽絲剝繭,探尋蛛絲馬跡,層層深刻,最後手指前方,自信地說:「真想只有一個!」
兇手被抓,這時是何等快感,debug是你與程序的摩擦,是你與框架爲數很少的交流與合做。
這時你不已再是一個API Caller,而是Program Coder,一位邏輯大偵探。
也許發現bug源頭是個低級錯誤,你會拍着大腿,大罵本身4843,而後仰天長笑。
也許看似沒有什麼用處,但整個流程下來你完成了一次發現問題,解決問題的思惟探索過程,
你作了一件和伽利略,牛頓,愛因斯坦同樣的事:經過思考和實踐解決問題
咱們重要的是解決問題嗎?更重要的是解決問題的過程和成長。框架
void main() => runApp(Text("Debug")); 爲何報錯!!!
複製代碼
這是筆者進入Flutter世界中的第一個疑問。Text也是Widget,爲何直接會崩潰?
從這個問題出發,一塊兒來場debug之旅吧。注意:本文不注重講知識點,重在debug的操做。
debug的重要性我就不說了,程序員不會debug,就像廚師不會用菜刀。
debug不止是尋找錯誤的,更重要的是輔助邏輯的分析,在分析中你也能夠學到不少知識。less
首先打斷點,在左側點擊,會出現一個小紅點,當debug模式運行時,程序會停在這裏,
也就是兇案已經發生,你讓整個世界暫停,以便你這個大偵探進行調查。ide
運行後會出現以下面板:你開始集結你的偵探團,面板的每一個功能都是你的小夥伴。
他們有各自的特定和能力,會助你一塊兒尋找兇手。函數
分別介紹一下:下面是小折,小藍和小紅。學習
小折:不拘小節,統觀全局,一行一行向下執行,碰見方法不會進入。口號:「大丈夫不拘小節」
小藍:心思縝密,收放有度。若是其中有可執行單元 (非系統),則進入。口號:「走,進去探險吧。」
小紅:細如絲縷,貫穿全局,即便是系統的源碼,也會進入一探究竟。口號:「只要功夫深鐵杵磨成針。」ui
因此這三人有各自的特色,偵查粒度依次更精細,能夠根據狀況酌情使用:
提個問題:當前狀態點三位偵查員分別什麼狀況?this
小折:哥不拘小節,執行下一行 : 控制檯直接報錯
小藍:偵探怎麼能像小折這麼隨意,遇到可執行單元,固然要去偵查一下,因而到達Text構造
小紅:姐很忙,在非系統方法調試時,我和小藍是同樣的。小藍進不去的,再來找我。
複製代碼
這裏要調試,固然選小藍,而後會發現進入了Text的構造方法,其中的變量區顯示着當前類的數據成員spa
再點一下小藍能夠發現它跳到了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方法,將剛纔的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。
這樣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
複製代碼
你會發現有執行到了RenderObjectElement#mount
只是這時的parent是咱們傳入的Text,由於這時時對Text的自件RichText進行裝載
也許你並不知道RichText是經過RenderParagraph進行建立RenderObject的 而這一開始的錯誤即是來源於這個斷言
這樣就也找到了異常所在。
這裏爲了帶你們多瞭解一些知識,因此跳的比較細,其實不少地方能夠用小折直接過
不太小藍能夠幫助你分析程序的運行邏輯,對你把控整個框架有很大的幫助。若是是學習能夠用小藍,更加細緻。
好比在inflateWidget這裏再打個斷點,運行時,首先會停留在第一個斷點mian那裏
可是之間的邏輯已經不須要再看了,使用不想一點點調試,那多斷點就能夠幫助你。
點擊這個,當前斷點就會被放行,程序繼續運行,當運行到下一個斷點時就會停下,也就是inflateWidget處,這樣就能夠避免調試中間的流程。當你在調試時,能夠先選一些肯出錯的地方,打上斷點,而後再去調試。這樣能更迅速的定位到bug所在。
也許你會怕斷點太多怎麼辦?這裏能夠對斷點進行查看和修改。
Run to Cursor可讓程序運行到指定光標處,注意它碰到其餘斷點會先停留在斷點處
最後說一下變量觀察和循環調試:
若是變量過多,能夠經過Watcher進行單獨觀察,點加號,輸入變量名便可
若是有一千萬次循環,一步一步還不得地老天荒,這時候循環調試能夠幫到你
你能夠指定一個條件,那麼下次循環就會變成此條件。
好了就說這麼多。