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