貝殼Flutter調試工具-FDB

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"開源地址","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GitHub地址:","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/LianjiaTech/flutter_fdb_package","title":"","type":null},"content":[{"type":"text","text":"https://github.com/LianjiaTech/flutter_fdb_package","attrs":{}}]}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1.前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前Flutter在貝殼的使用量越來越高,業務中Flutter頁面達到600+,甚至在某些業務線Flutter頁面佔比達到70%。這種狀況下我們迫切需要一個功能完善、體驗流暢的Flutter調試工具。調研市面上Flutter調試工具之後,結合我們公司的業務特點,開發了自己的Flutter調試工具——FDB。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文將簡要介紹FDB有哪些功能,並重點介紹核心功能是如何實現的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2.FDB有什麼用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FDB(Flutter Debug)不僅僅是隻面向開發過程的工具,也解決性能優化、設計走查、QA測試等環節的痛點問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Flutter的研發過程中,您肯定遇見過以下問題:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設計反覆確認Widget的大小、對齊等UI問題","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於不知道包中代碼是否是最新代碼,導致重新打包測試","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"開發過程中反覆設置背景色,來確定Widget的邊界範圍","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"性能優化時,沒有快捷的工具查看內存使用情況、內存泄漏情況","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"...","attrs":{}}]}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,我們開發了以下功能,爲開發者在開發過程中或優化性能時,提供精確的數據支撐和問題定位。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d1/d166bc9fdd554fe3a68ea29a5800dbed.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看出FDB的功能包含:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"組件信息檢查","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"內存詳細數據的展示","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"內存泄漏自動檢測","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FPS檢測","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"源碼文件和源碼具體行數的展示","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些功能都需要我們在合適的節點中獲取,這些節點穿插在Dart代碼的運行流程中,這些功能的實現需要我們對Dart代碼的運行流程有大概的瞭解,這樣我們知道在哪個節點能獲取哪些信息。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3.前置知識點","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總的來說,Dart代碼的運行可分爲:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"前端編譯階段、虛擬機運行階段","attrs":{}},{"type":"text","text":"。接下來,我們從編譯、運行兩個方面來說。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Dart VM有多種方式運行Dart代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• JIT模式下運行源碼或者Kernal binary","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• 通過snapshot的方式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鑑於本文描述的使用場景,我們以Debug模式爲例來介紹Dart VM是如何運行我們的代碼。","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1 Flutter前端編譯器","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Flutter Debug模式下我們的Dart源碼被gen_kernel處理成Kernal binary,也就是dill文件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以在Debug產物中找到該dill文件,這便是Kernal binary。Kernal是一種從Dart衍生而來的高級語言,用於分析和進一步的轉化。Kernal binary就是該語言的描述,它包含了序列化的Kernel ASTs以及內存標識。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Kernel AST","attrs":{}},{"type":"text","text":"是CFE(前端編譯器 common front-end)生成,有語義分析的作用,可交付於Dart VM、dev_compiler以及dart2js等工具直接用於語義的分析。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"內存標識","attrs":{}},{"type":"text","text":"可以序列化成可被虛擬機運行的機器碼,類似於Class和JVM的關係。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述的前端編譯流程如下圖所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1c/1cc0da9a6ce36a5561aacc820eaab162.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#ffffff","name":"user"}}],"text":"snapshot_delegate","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過以上簡單的介紹,我們瞭解了Debug模式下代碼是如何編譯成產物的。接下來我們重點來看:編譯的產物是如何被VM運行的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2. JIT產物的運行","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.1. VM的初始化工作","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"虛擬機要執行任務需要初始化以下幾個功能模塊:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"運行時系統: ","attrs":{}},{"type":"text","text":"負責運行期間代碼的裝載(懶加載)和釋放。比如:對象的實例化、類成員信息的讀取,方法的調用,內存GC,並保存了簽名信息(快照)。由此可知,我們獲取內存信息、方法調用鏈等運行時數據或主動觸發GC都是在此處。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"service協議: ","attrs":{}},{"type":"text","text":"這是VM爲方便我們拿到運行時數據而開啓的一個服務,連接此服務之後,我們就可以通過Dart VM的相關協議,拿到運行時系統的數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"其他","attrs":{}},{"type":"text","text":":包含核心庫原生方法、編譯流水線、解釋器、ARM 模擬器。鑑於本文不涉及VM的其他部分,暫不講解。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下圖爲VM的整體結構:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/23/232a7847310628e678817b39a9ea3a4a.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總結一下:運行時數據代表了Dart虛擬機的具體運行狀態,比如","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"內存快照、對象分配、方法調用","attrs":{}},{"type":"text","text":"等等。並且我們可以通過service協議來獲取運行時數據,像Dev-Tools、Android Studio中的調試功能都是通過這個協議來實現的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"那麼,如果我們想獲取當前虛擬機的內存信息、內存中的Class、Class的實例、實例的具體信息、方法調用鏈等屬性,就需要藉助此Service去找運行時數據要。具體的如何通過Service獲取VM運行時中的數據,後面的2.4補充知識點中會詳細介紹。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OK,到此VM已經具備了執行我們Dart代碼的能力。因爲Dart VM所需要的運行數據包含在Flutter的“三棵樹”中,接下來,我們來看Flutter的“三棵樹”。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.2. Flutter的“三棵樹”","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們開發的代碼,從runApp開始到頁面展示出來,期間Flutter會用我們的代碼生成“三棵樹”:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Widget樹","attrs":{}},{"type":"text","text":":是開發者使用Flutter對一個頁面的描述;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Element樹","attrs":{}},{"type":"text","text":":每一個Widget都會有一個Element與之對應,因此Flutter會根據Widget樹生成Element,並且Element樹是Widget樹轉化爲RenderObject樹的中間產物,起到“上下文”的作用;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"RenderObject樹","attrs":{}},{"type":"text","text":":真正用於繪製的樹,包含了尺寸等具體佈局信息;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/78/783e112a866f3c6c3067974bd90a0e57.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由此可見“三棵樹”關係如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• Widget可通過createElement()方法創建Element。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• Element通過調用Widget的createRenderObject()方法創建RenderObject","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• Element直接持有Widget和RenderObject。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• RenderObject通過DebugCreator包裝器的方式間接持有Element。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"那麼,如果我們想獲取頁面上某個元素的屬性,找到Element就是關鍵,因爲Element作爲上下文,可以拿到具體的Widget及RendObject,從而拿到其屬性及佈局信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OK,到此,我們知道VM將我們代碼通過三棵樹的原理轉化爲可渲染的對象,從而渲染出來,而且我們可以通過Element獲取到Widget,需要注意的是“三棵樹”給我們的信息是組件的靜態信息,但是如果我們想獲取某個Widget的輪廓等動態信息以及對應我們的源碼文件名和行列等信息那就要介紹另兩個概念:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"WidgetInspector和WidgetInspectorService","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.3. WidgetInspector和WidgetInspectorService","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter的入口main函數中,會使用runApp()創建一個WidgetsApp,WidgetsApp便會在Debug模式下爲我們開啓一個WidgetInspector。那麼WidgetInspector是什麼呢,WidgetInspector和WidgetInspectorService之間有什麼關係呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"WidgetInspector","attrs":{}},{"type":"text","text":":可以檢查屏幕上一個widget的結構,包括輪廓、屬性及對應源碼的位置。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"WidgetInspectorService","attrs":{}},{"type":"text","text":":WidgetInspector實例不能被用戶直接獲取使用,WidgetInspectorService爲用戶提供了全局的單例,用來操作WidgetInspector從而獲取我們想要的信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"那麼,如果我們想獲取界面上某個具體Widget的輪廓、屬性及對應源碼的位置信息,就需要通過WidgetInspectorService.instance單例去獲取。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.2.4. 補充知識點:怎麼在VM運行時中獲取數據","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們知道VM運行時是C++開發的,在Flutter中直接獲取VM的運行時數據需要藉助一箇中間件,幸運的是官方爲我們提供了Service,藉助Service的能力,我們便可以實現拿到VM 運行時的數據。既然是Service,那麼就需要一個數據傳輸的協議。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"協議雙方遵循C-S架構,虛擬機作爲遠端服務來響應工具的請求,請求和響應的格式是Json。服務端暴露了很多接口,比如獲取版本信息,獲取內存快照、獲取方法調用和對象信息等等。我們以getVersion爲例看一下調用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調試工具以下面的格式發出請求:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"{\n \"jsonrpc\": \"2.0\",\n \"method\": \"getVersion\",\n \"params\": {},\n \"id\": \"1\"\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務端以下面的格式返回響應數據:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"type\": \"Version\",\n \"major\": 3,\n \"minor\": 5\n }\n \"id\": \"1\"\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外,虛擬機通過ObjRef、Obj、id這三個字段,來描述一個具體的對象。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"ObjRef","attrs":{}},{"type":"text","text":":對象的基本信息,比如name、id,但是不包括完整信息,我們可以把它理解成是一個對象指針。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"•","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"id","attrs":{}},{"type":"text","text":":即ObjRef中的id,它是對象的唯一標識,VM通過id來識別一個對象。舉個例子,如果你想獲取一個對象的詳細信息,就需要給VM該對象的id。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Obj","attrs":{}},{"type":"text","text":":對象的詳細信息,也就是我們通過id在VM處獲取到的完整對象。它包含該對象所有的信息,比如字段、父類等等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"圖示三個字段的關係:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5c/5cb99435296c78cb1235d852077d2dbe.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣一個完整的數據就從虛擬機獲得了,那虛擬機具體支持哪些接口呢?大家可以看這一篇","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md","title":"","type":null},"content":[{"type":"text","text":"官方文檔","attrs":{}}]},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般常用接口如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getAllocationProfile","attrs":{}},{"type":"text","text":":獲取當前運行內存中Class、實例等的內存使用數據,另外該方法可獲取每次GC的時間戳,還可以主動觸發GC。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getClassList","attrs":{}},{"type":"text","text":":獲取VM中所有類。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getInstances","attrs":{}},{"type":"text","text":":獲取某個類所有實例的引用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getObject","attrs":{}},{"type":"text","text":":獲取指定實例的類型、內存大小、變量等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getScripts","attrs":{}},{"type":"text","text":":獲取所有文件和文件對應id的list。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OK,具備了以上知識,我們進入下一部分:FDB的具體實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FDB一共分爲三類:UI相關、性能優化相關、功能代碼相關。下面着重介紹:性能優化相關、Widget拾取、頁面代碼工具的核心原理與實現。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4.性能優化","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們知道性能優化的成果,需要依賴數據指標佐證,Flutter雖然提供了Dev Tools和Observatory,來幫助我們開發者採集性能數據,但是操作複雜,上手難度高。比如:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"link","attrs":{"href":"https://flutter.dev/docs/development/tools/devtools/memory","title":"","type":null},"content":[{"type":"text","text":"內存檢測","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• ","attrs":{}},{"type":"link","attrs":{"href":"https://flutter.dev/docs/development/tools/devtools/performance","title":"","type":null},"content":[{"type":"text","text":"幀率檢測","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"都需要先在Android Studio連接上應用的前提下,在網頁上具體操作相應的工具,網頁失敗次數較高,等待時間較長。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此根據原生優化的經驗,我們提供了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"內存信息、內存泄漏檢測、幀率檢測","attrs":{}},{"type":"text","text":"三個工具,功能如下所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/02/0206c6c440fe5ca57f77665803caf642.gif","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"左圖是內存泄漏演示,演示了整個檢測的過程以及檢測結果。中圖是內存信息演示。右圖是幀率檢測演示。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.1. 內存信息","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說到內存信息,回憶上面的知識,我們想到的是內存數據一定在VM運行時中,對應我們1.2.1章節的內容。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"內存信息主要包含:類信息獲取和對象信息的獲取。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"類信息獲取","attrs":{}},{"type":"text","text":":虛擬機的getAllocationProfile協議可以獲取線程的所有信息:包含了class、已經使用了多少內存、GC時間戳等。以及虛擬機的Isolate線程對象封裝了Library信息。依靠類和Library就可以進行分組。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"對象信息","attrs":{}},{"type":"text","text":":虛擬機的getInstances協議,可以獲取某個類下面所有實例的id,通過獲取到的實例id和getObject協議,可以查到具體的實例實體:類型、所有的字段、內存大小等。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲取到虛擬機的原始數據後,通過我們的加工,使展示給用戶的內存信息數據","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"更簡潔,更有用","attrs":{}},{"type":"text","text":":以分組的方式只展示內存中的類信息,並且不僅可查看類信息,還具體到了對象信息、屬性信息。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.2. 內存泄漏檢測","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同Java類似,Dart 語言也具有垃圾回收機制,有垃圾回收就避免不了會內存泄漏。那麼如何檢測內存泄漏呢?關於內存泄漏檢測的核心原理,","attrs":{}},{"type":"link","attrs":{"href":"https://flutter.cn/community/tutorials/memory-leak-monitoring-on-flutter","title":"","type":null},"content":[{"type":"text","text":"這篇文章","attrs":{}}]},{"type":"text","text":"有很好的描述","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"總結來說,就是使用弱引用引用待觀測對象,並在合適的時機,發起虛擬機GC任務,然後檢查弱引用的對象是否爲null。如果不爲null,說明發生了內存泄漏。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於我們是從頁面的維度檢查內存泄漏,那麼待觀測對象就是Flutter頁面——Widget、State、Element。那時機就是頁面的打開和關閉了,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"頁面打開的時候進行觀測,頁面關閉的時候進行檢查","attrs":{}},{"type":"text","text":"。檢測流程如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/17/17601027419f03fda447a25c7deb8924.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們自定義了Route觀察者,把檢測的任務封裝到自定義的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"NavigatorObserver","attrs":{}},{"type":"text","text":"中,比如didPush、didPop方法。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於Route觀察者,一般的做法是 業務方給自己的MaterialApp的navigatorObservers屬性賦值。但是,我們FDB的核心原則是:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"更少的侵入業務代碼","attrs":{}},{"type":"text","text":",所以我們使用自動監聽的方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/25/25de7e264d1085a7c7e8c8aadd2233c4.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上圖可知,左側的方式需要業務方自己設置,而右側的方式業務方代碼不需要任何變動。那麼自動方式是如何實現的呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用業務代碼的子節點,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"向上查找到業務的Navigator節點,併爲此Navigator新增一個路由觀察者","attrs":{}},{"type":"text","text":"。這樣就解決了兄弟節點無法查找Navigator以及業務代碼手動綁定的問題。核心代碼如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/17/171e071fa648a7de825eedfe9ef0594c.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上面的泄漏演示流程,我們可以看到,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"最終的結果完整的表達出了泄漏鏈和具體泄漏的代碼行數","attrs":{}},{"type":"text","text":",開發者就可以根據具體代碼行數,進行修復。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.3. 幀率檢測","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"幀率檢測的數據來自","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"SchedulerBinding","attrs":{}},{"type":"text","text":"。Flutter中包含多個Binding,每個Binding都是是Flutter的“膠水粘合劑”,而SchedulerBinding就是粘合了繪製相關的任務,比如調度幀scheduleTask、回調幀_handleBeginFrame、幀時間回調addTimingsCallback等等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中addTimingsCallback方法就是關於幀時間的回調,只要有幀被繪製了(setState、動畫等刷新),該回調會被執行,給我們的回調數據是數組的FrameTiming。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FrameTiming包含了這一幀的總時長、光柵時長、build時長等等,根據這些信息就可以算出幀率、耗時幀等等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/be/be13e2c3537e3f9f983eeb44e637a873.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"5.Widget拾取","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家研發過程中可能會遇到:無法快速定位頁面上的Widget在源碼中的位置;查看某個Widget的邊界範 圍,必須依賴IDE;UI、UE走查時無法動態查看文本過長或過短的邊界情況。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過UI拾取工具以上問題都可以很好的解決","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下是UI拾取工具的功能演示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ad/adb1c01a84550da67917311af40c7541.gif","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上面的演示,我們可以看到UI拾取工具的基本功能:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"自由拖動拾取器來標記Widget範圍;獲取Widget的代碼文件和行數、文本組件編輯文本等等。細看功能其實能夠發現,我們獲取的就是某個組件的輪廓、屬性及對應源碼的位置信息,我們想到的是什麼?沒錯就是WidgetInspector,對應1.2.2和1.2.3章節的內容","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據上面的內容,該功能的思路如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然Element是Widget和Render的橋樑,那麼我們先獲取座標對應的Element,然後利用WidgetInspector去獲取Widget的輪廓、源碼等信息即可。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上圖看,Element是Widget和Render的橋樑。因此,只要我們找到Element,理順了三棵樹的關係,功能實現就有了突破口。實現過程如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1. 找到座標選中的元素。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"層序遍歷Element樹,比對 拾取座標和 Element持有的Render的範圍,最深層級的元素就是選中元素。查找流程如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過上圖的四步之後,edgeHits數組的第一個元素(最深層級)就是目標Element。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6c/6ce2e8a89b966cbf65d103e8d7a39f35.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"2. 獲取所需控件信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WidgetInspectorService的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getSelectedSummaryWidget","attrs":{}},{"type":"text","text":"可以通過told方法返回的id獲取對應的Widget信息(如下圖所示),包括:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"組件類型、代碼路徑和行數","attrs":{}},{"type":"text","text":"等等。這兩個方法配合使用就可以拿到所需信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8e/8ede08fe67d39cedf937847ffb980335.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的json結構中,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"description字段","attrs":{}},{"type":"text","text":"是Widget類型,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"creationLocation字段","attrs":{}},{"type":"text","text":"是創建Widget的代碼位置。有了具體的代碼行數,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"即使不是自己的代碼,也可以快速定位,快速進入開發","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"3. 編輯文本。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文本編輯功能是和市面同類產品的創新點,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"不僅能看還能編輯","attrs":{}},{"type":"text","text":"。設計的同學非常需要編輯文本功能,因爲設計同學不會本地mock數據,數據是什麼,走查就只能看到什麼,想要查看文本過多或者過少的情況,每次都需要依賴後端同學模擬,所以經常出現走查不徹底、走查成本高的問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從實現的角度來看,該功能較爲複雜,原因有:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"無直接入口","attrs":{}},{"type":"text","text":": 拿不到State對象,無法調用業務方的更新方法(setState)。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"影響節點要少","attrs":{}},{"type":"text","text":": 不能直接從根節點就開始更新,造成不必要的損耗。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"更新要臨時","attrs":{}},{"type":"text","text":": 不能影響真實的代碼屬性。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鑑於以上原因,實現方法較爲巧妙:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"臨時生成新文本組件,主動觸發Element更新和繪製,只更新選中的Element。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/0a/0ae47642152ae7ee084fe84ea1ef6f76.png","alt":null,"title":"處理的流程圖","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"左圖是處理的流程圖,右圖是具體的核心代碼。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"6.頁面代碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"測試過程中,我們經常對QA說,“你的包是不是沒有我提交的代碼啊,你的包不是最新包吧,...”。查看Flutter代碼的功能,對解決上面的問題有一定的幫助。工具的效果演示如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6d/6df9880acf54ed5677198bdc56d71c59.gif","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WidgetInspectorService的selection屬性可以獲取到一個Element,並使用Element綁定的Widget就可以獲取文件路徑,但是我們的Widget拾取功能、Android Studio等其他調試工具可能會變動到這個值。導致WidgetInspectorService的Element可能不能直接找到當前頁面。FDB根據Flutter頁面疊加的原理來找到當前的頁面,查找方式如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter的根節點是Overlay組件,該組件是一個可以管理Widget的棧。如果將一個Widget插入這個棧上,就可以讓此Widget浮在其他的Widget之上。而且這個Overlay組件是被Navigator所創建。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們開發的頁面,就這樣被一個個的疊加到了Navigator的Overlay上。","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"所以,只要能拿到Overlay,頁面代碼問題就有了突破口","attrs":{}},{"type":"text","text":"。因爲Overlay保存了一個個的Route,最頂層的Route就是當前頁面的Route。根據Route封裝的WidgetBuilder就拿到了當前頁面Widget是誰。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再結合上面介紹的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"getSelectedSummaryWidget","attrs":{}},{"type":"text","text":"方法,就得到了當前頁面的文件名。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Dart虛擬機的getScripts方法可以獲取所有庫文件的 Id和文件名,對比文件名獲得目標文件的 Id。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Dart虛擬機中眼中,文件也是Object,也可以通過Id進行getObject操作。這樣最終就拿到了頁面源碼。流程如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7e/7ef00400d21c47f9692d781fd130937a.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"7.總結與展望","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面介紹了FDB的功能,以及核心工具的實現,相信大家對FDB、Flutter工具建設有了一定的認識。FDB的每一個功能都依賴虛擬機數據,掌握Flutter運行中的每一個節點,是我們能獲取所需數據的支撐。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前第一版本的Flutter調試工具已經完成,在貝殼B端已有3個APP接入,接入之初兩週時間使用次數已經突破2000。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FDB項目已開源,後面會根據各業務方及社區內開發者的反饋進行下一步的迭代和調優,以提高大家開發需求和排查問題的效率。同時我們鼓勵Flutter社區開發者們參與FDB的共建或者多提些建議、反饋。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"8.參考文獻","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們的實現是站在前人的肩膀山續接探索的結果,這裏特別感謝一系列開源的作者,是你們爲Flutter更好的落地保駕護航。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://pub.flutter-io.cn/packages/flutter_ume","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://pub.dev/packages/dokit","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://pub.dev/packages/leak_detector","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https://flutter.cn/community/tutorials/memory-leak-monitoring-on-flutter","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章