解讀京東零售云mPaaS中Flutter中熱重載原理
本文要點:
- 了解京東零售云mPaaS中Flutter的熱重載原理,有利于日常開發中高效排查問題;
- 掌握如何調試斷點Flutter工具鏈源碼;
一、前言
1.1 熱重載是什么?
熟悉JS的同學,可能會嗤之以鼻,在N年前就已經用上熱重載了,但是對客戶端開發人員來說,簡直是福音。
那先來看下Flutter官方的定義:
- Flutter’s hot reload feature helps you quickly and easily experiment, build UIs, add features, and fix bugs. Hot reload works by injecting updated source code files into the running
- Dart Virtual Machine (VM). After the VM updates classes with the new versions of fields and functions, the Flutter framework automatically rebuilds the widget tree,
- allowing you to quickly view the effects of your changes.
簡單來說,就是通過將修改后的源代碼文件注入到正在運行的 Dart 虛擬機來實現,注入之后, Flutter 會自動重新構建 widget 樹。
1.2 為什么需要熱重載?
程序猿在刀耕火種的時代,開發調試是這樣子的:
當項目不大,人數不多的情況下,畫面是非常和諧的,效率也是毋庸置疑的高效。但現實是,在大公司,項目往往很大,編譯巨慢無比,同時開發人員眾多,有著非常嚴格的流程制度,導致看起來本沒有問題的開發調試流程,變得異常的痛苦,降低了個體的效率,這里強調下,指的是個體的效率,個人認為越是完善的流程體系,對個體的約束往往越強,但從團隊的角度去看待效率,一定是能 1+ 1 大于 2 的。
而此時的心情是這樣子的:
而有了熱重載,開發調試是這樣子的:
心情也就成這樣子的:
1.3 拋出問題
從熱重載定義來看,不少人腦子里蹦出不少跟我一樣的疑惑:
- 怎么知道哪個文件被修改?
- 修改的源代碼到底被轉成什么?
- 修改的源代碼是怎么注入到Dart虛擬機的?
- Flutter框架又是怎么觸發widget重繪的?
同時在日常使用熱重載的過程中,也會碰到不少這樣那樣的疑惑:
- 為什么運行flutter attach后還需要手動輸入r來熱重載?
- 手動敲r,這么無(gou)語(shi)的設計,我們能做成自動化嗎?
當你在網上看過大量熱重載文章后,又衍生了額外的問題:
- 嘗試去探索源碼時,case太多,怎么能模擬真實環境?能否斷點調試Flutter源碼?
- 熱重載看著跟動態化很像,那能否運用在動態化技術上?
不急,本文會對上述疑問進行一一解答。
二、dart的熱重載
由于Flutter采用dart作為開發語言,我們先從dart角度來驗證下熱重載。
2.1 編寫驗證demo
考慮到dart執行完會關閉當前進程,我們寫了個定時器來保證進程存活,同時能看到熱重載效果。
2.2 開啟VMService
終端下執行 dart --enable-vm-service main.dart,其中的main.dart為2.1中代碼文件:
可以看到終端會不斷輸出"Hello JDFlutter"的字符。
2.3 執行熱重載
我們將main.dart文件中打印日志修改為”Hello JD”,同時打開終端輸出的Observatory鏈接地址,如下:
找到我們main.dart的Isolate(讀者可以簡單理解為是dart中的線程,只不過Isolate沒有共享內存),圖中紅圈部分,進入后找到Reload Source:
點擊Reload Source后,終端開始輸出”Hello JD”的字符,完成了一次熱重載過程,如下圖:
2.4 自動化熱重載
還是以上面為例子基礎例子,我們加入文件監聽,并且通過發送消息給vm_service來實現熱重載,代碼如下:
直接運行 dart --enable-vm-service main.dart,期間修改”Hello JDFlutter”為”Hello JD”,運行結果如下:
可以看出,我們成功實現了自動化熱重載,上述代碼跟Dart虛擬機通信步驟如下:
- 獲取Dart VM的websocket服務URI
- 通過URI連接上Dart VM的service
- 通過service獲取Dart VM
- 通過Dart VM獲取isolateId
- 通過service重載指定isolateId的任務
2.5 Dart虛擬機可做的事情
到這里,大家可以放飛自我,Dart Service提供了大量對外協議,包含斷點、獲取虛擬機狀態,性能等協議,可以參考:Dart虛擬機服務接口。
三、Flutter的熱重載
Flutter的熱重載,本質是在封裝dart熱重載并且對不同的設備啟動安裝加載等流程,接下來準備好在Flutter源碼世界里翱翔吧,以下分析基于v1.22.5分支的源碼。
俗話說,工欲善其事必先利其器,在源碼翱翔久了,容易迷茫,找不到東西南北,看到關鍵方法,又不知道是不是代碼真實的case,需要能驗證我們的想法,最簡單的辦法打斷點,有針對性的去看源碼。
3.1 IDE斷點
Flutter源碼的下載也很簡單,這里就不贅述了,大家可以上網搜下。Flutter工具鏈的源碼位于packages/flutter_tools下。
本文是通過Android Studio(比較熟)來配置和查看源碼,配置如下:
- 第一步,先新建一個運行配置,選Dart Command Line App;
- 第二步,找到Flutter源碼中工具鏈的入口文件,flutter_tools.dart;
- 第三步,輸入想運行的命令;
- 第四步,找到要調試的Flutter工程;
一頓配置下來,就可以用工具鏈完美的debug指定Flutter工程的源碼,接下來就是選好設備,點擊debug按鈕,如下圖:
3.2 整體流程
以下是Flutter熱重載流程圖:
簡述為:
- 代碼改動:工具會掃描工程下的文件,通過修改時間來比對哪些文件被修改;
- 首次編譯:第一次啟動會生成全量app.dill文件;
- 增量編譯:對修改的文件編譯生成app.dill.incremental.dill增量文件;
- 更新文件:將增量產物推送到設備中;
- UI更新:DartVM收到增量文件后進行合并,并通知Flutter引擎更新UI
整個過程并沒有讓App重啟,從而達到高效開發調試效果。
3.3 源碼分析
3.3.1 run命令流程
我們從flutter run命令為入口分析,類位于packages/flutter_tools/lib/executable.dart中的main()方法,run命令最終實現類位于packages/flutter_tools/lib/src/commands/run.dart。
RunCommand在構造函數中默認開啟了hot標識,如果需要關閉,要新增入參--no-hot。
從run命令的流程,可以看出,主要是做了默認參數設置,參數校驗,flutter設備初始,模式判斷等,熱重載是從HotRunner.run中開始執行。
3.3.2 熱重載流程-首次啟動
在HotRunner中,流程也并不復雜:
可以看出,HotRunner做了三件事:
- 對目標設備,編譯生成dill文件(有人叫kernel文件,本質是一種中間描述,后文會介紹);
- 對目標設備,安裝運行App;
- 對目標設備進行attach,從而開啟attach;
第二步會涉及到不同平臺不同做法,對iOS和Android來說,分別對應xcrun和adb,不是本文重點,流程也比較長,以后有機會再展開講,重點說第一步和第三步。
編譯生成dill文件
最終調用到_compile方法,代碼太過于繁瑣,我們直接斷點看,如下:
從斷點信息可以獲知,dart文件會被轉為kernel文件app.dill,以下截取部分app.dill內容,可以看出app.dill是一份完整的代碼文件,包含了main.dart的內容,右邊為main.dart源文件,左邊為app.dill文件內容:
生成的app.dill是一份全量的代碼,接下來編譯不同設備(Android、iOS)的安裝包,同時運行指定的包。
此時生成app.dill的進程,我們暫且稱為“編譯進程”,后續熱重載增量的dill,也是驅動該進程生成。
attach設備
在上述的第二步,設備在啟動運行App時,會打開App中DartVM的Observatory服務,本質是一個websocket服務,按照自定義的jsonrpc2.0協議進行通信,在attach時,會通過URI連接上設備服務,如下圖:
連上DartVM服務后,會注冊幾個熱重載事件:reloadSources,reloadMethod,hotRestart,這幾個事件并不是注冊到App中的Dart虛擬機,而是提供給flutter tool其他命令使用,如下圖:
同時通過DartVM服務,來初始設備中flutter產物,設備中產物路徑是臨時生成,用XXX代替,產物路徑為:
- Android中為:file:///data/user/0/com.example.flutter_app/code_cache/XXX/flutter_app/
- iOS模擬器中為:/Users/hexianting/資源庫/Developer/CoreSimulator/Devices/BC003085-8F19-4EF3-AB84-BD44282F79B7(模擬器設備ID)/data/Containers/Data/Application/745DE582-59F1-4193-9692-131E611A9359/tmp/XXX/flutter_app/
具體代碼如下:
3.3.3 觸發熱重載
下面分別從源碼角度,看看到底做了什么?
開發者在執行flutter run或者flutter attach后,在終端中輸入r,即可體驗到重載效果,如果在Android Studio和VSCode中,直接Ctrl+S或者Cmd+S即可。
對應到源碼入口:
不管是HotReload還是HotRestart,最終都是調用HotRunner.restart方法,一路跟進,最終會到某個具體設備update方法,并再次調用上述《熱重載流程-首次啟動》中的_compile方法,通知編譯進程生成增量的dill文件app.dill.incremental.dill。那這個增量文件到底是什么呢?demo中修改字符串"Flutter Demo Home Page"為"Flutter Demo Home Page2",來看看dill文件內容:
第一張圖為修改前,第二種為修改后,第三張為增量的dill內容。可以看出增量的dill文件僅包含改動的dart文件代碼。
生成增量的dill后,會通過_DevFSHttpWriter寫入設備,如下圖:
當同步完增量文件,最后還需要通知DartVM去刷新UI界面,這個步驟就跟我們上述的2.4節內容類似:
vmService.reloadSources最終調用了_call方法,這是一個dart官方庫,如下:
HotRestart與HotReload區別
Flutter官方提供兩種快速調試方法,一種是HotReload,另一種是HotRestart。前者無感知局部刷新,體驗最好,但是缺點也很明顯,適用比較局限,可以參考官網給出樣例:HotReload,主要有這幾種場景不適用:
- enum改成class類;
- 字體修改;
- 泛類型修改;
- Android和iOS原生修改;
而在HotRestart流程中,相比HotReload流程,增加了清除資源操作,同時不再生成增量的dill文件,每次改動都是生成全量的app.dill文件,該細節就不展開,感興趣讀者可以debug源碼看。
上述可以看出HotRestart額外處理了一些事情,包括殺掉非UI的isolate,重置UI的isolate等。
對于dill文件同步到設備中位置,不同設備不一樣:
- Android:file:///data/user/0/com.example.flutter_app/code_cache/XXX/flutter_app/lib/
- iOS模擬器:/Users/hexianting/Library/Developer/CoreSimulator/Devices/BC003085-8F19-4EF3-AB84-BD44282F79B7(模擬器設備ID)/data/Containers/Data/Application/9C8E4694-AC99-4A5C-BC46-63567F1C6FD9/tmp/XXX/flutter_app/lib/
至此,熱重載源碼就告一段落,很多奇技淫巧并不能一一展現,值得大家動手去看看。
四、總結
經過上述一頓探索,文章最早提出的幾個疑問,想必都有了答案。這里只是介紹了Flutter源碼的冰山一角,更多源碼還需要繼續探索,通過閱讀源碼,可做的事情很多:
- 通過文件監聽+vm_service通信,干掉手動輸入r或者R的這種無(gou)語(shi)設計;
- 源碼中并沒有限制多個設備,flutter run同時運行在多個模擬器中,并開啟熱重載;
- iOS模擬器不重新安裝App的情況下,直接替換模擬器中的flutter產物,以達到快速調試手段;
- debug狀態下的DartVM可以通過熱重載來動態化,但性能較低,與谷歌Flutter的高性能目標不符;
總之,可做的事情很多,那我們看源碼的意義就非常清晰:
- 深入了解Flutter運行機制,去定制Flutter框架;
- 通過研究這些頂級工程師的實現思路,去完善我們自己的邏輯體系,從而成為一個更加嚴謹的人。
五、參考資料
- https://flutter.dev/docs/development/tools/hot-reload
- http://gityuan.com/2019/09/07/flutter_run/
- https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md
- http://static.kancloud.cn/alex_wsc/flutter_demo/1570089