Flutter的Hot Reload是如何做到的

眾所周知簇宽,在Flutter 應(yīng)用的Debug模式下苔巨,當(dāng)我們開啟【Hot Reload】功能時(shí)槐脏,不需要在重啟應(yīng)用即可看到最新的代碼效果店乐。這種類似于RN艰躺、Weex和小程序的熱加載功能是如何做到的呢,它背后的原理是什么眨八?

基本使用方法

Flutter的熱重載(hot reload)功能可以幫助您在無需重新啟動(dòng)應(yīng)用的情況下快速腺兴、輕松地進(jìn)行測(cè)試、構(gòu)建用戶界面廉侧、添加功能以及修復(fù)錯(cuò)誤页响。 通過將更新后的源代碼文件注入正在運(yùn)行的Dart虛擬機(jī)(VM)中來實(shí)現(xiàn)熱重載篓足。在虛擬機(jī)使用新的的字段和函數(shù)更新類后,F(xiàn)lutter框架會(huì)自動(dòng)重新構(gòu)建widget樹闰蚕,以便您快速查看更改的效果栈拖。

我們編寫一個(gè)應(yīng)用,運(yùn)行應(yīng)用程序没陡,然后修改 Flutter APP 工程里的 Dart 代碼涩哟,然后點(diǎn)擊【Hot Reload】按鈕開啟熱重載,如下圖所示诗鸭。


image.png

VS Code 開啟 Hot Reload 染簇。


image.png

當(dāng)我們修改Dart代碼,點(diǎn)擊保存的時(shí)候强岸,就會(huì)看到界面已經(jīng)發(fā)生了變化锻弓,如下圖。


image.png

總結(jié)一下蝌箍,在Flutter中使用熱重載需要經(jīng)過以下幾個(gè)步驟:

1青灼、連接真機(jī)或虛擬機(jī),運(yùn)行 Flutter APP妓盲,且必須以 Debug 模式啟動(dòng)杂拨。因?yàn)橹挥?Debug 模式才能使用 Hot Reload。
2悯衬、修改 Flutter APP 工程里的 Dart 代碼弹沽,但并不是所有 Dart 代碼的修改都可以使用 Hot Reload,有一些情況下Hot Reload 并不能生效筋粗,只能使用 Hot Restart(重新啟動(dòng))策橘。
3、使用快捷鍵 ctrl+s(Windows娜亿、Linux)或者 cmd+s(MacOS)丽已,或者點(diǎn)擊 Hot Reload 的按鈕,就完成了Hot Reload 的操作买决,
Hot Reload 成功后沛婴,會(huì)在 Debug Consol 中看到輸出如下類似的消息:

Performing hot reload...
Reloaded 1 of 448 libraries in 2,777ms.

工作原理

熱重載

熱重載是指,在不中斷 App 正常運(yùn)行的情況下督赤,動(dòng)態(tài)注入修改后的代碼片段嘁灯。而這一切的背后,離不開 Flutter 所提供的運(yùn)行時(shí)編譯能力够挂。為了更好地理解 Flutter 的熱重載實(shí)現(xiàn)原理旁仿,我們先簡單回顧一下 Flutter 編譯模式背后的技術(shù)吧。

JIT

JIT(Just In Time),指的是即時(shí)編譯或運(yùn)行時(shí)編譯枯冈,在 Debug 模式中使用毅贮,可以動(dòng)態(tài)下發(fā)和執(zhí)行代碼,啟動(dòng)速度快尘奏,但執(zhí)行性能受運(yùn)行時(shí)編譯影響滩褥。


JIT.jpeg

AOT

AOT(Ahead Of Time),指的是提前編譯或運(yùn)行前編譯炫加,在 Release 模式中使用瑰煎,可以為特定的平臺(tái)生成穩(wěn)定的二進(jìn)制代碼,執(zhí)行性能好俗孝、運(yùn)行速度快酒甸,但每次執(zhí)行均需提前編譯,開發(fā)調(diào)試效率低赋铝。


AOT.jpeg

可以看到插勤,F(xiàn)lutter 提供的兩種編譯模式中,AOT 是靜態(tài)編譯革骨,即編譯成設(shè)備可直接執(zhí)行的二進(jìn)制碼农尖;而 JIT 則是動(dòng)態(tài)編譯,即將 Dart 代碼編譯成中間代碼(Script Snapshot)良哲,在運(yùn)行時(shí)設(shè)備需要 Dart VM 解釋執(zhí)行盛卡。

而熱重載之所以只能在 Debug 模式下使用,是因?yàn)?Debug 模式下筑凫,F(xiàn)lutter 采用的是 JIT 動(dòng)態(tài)編譯(而 Release 模式下采用的是 AOT 靜態(tài)編譯)滑沧。JIT 編譯器將 Dart 代碼編譯成可以運(yùn)行在 Dart VM 上的 Dart Kernel,而 Dart Kernel 是可以動(dòng)態(tài)更新的巍实,這就實(shí)現(xiàn)了代碼的實(shí)時(shí)更新功能嚎货,原理如下圖。


熱重載原理.jpeg

總體來說蔫浆,完成熱重載的可以分為掃描工程改動(dòng)、增量編譯姐叁、推送更新瓦盛、代碼合并、Widget 重建 5 個(gè)步驟外潜。

  • 工程改動(dòng)原环。熱重載模塊會(huì)逐一掃描工程中的文件,檢查是否有新增处窥、刪除或者改動(dòng)嘱吗,直到找到在上次編譯之后,發(fā)生變化的 Dart 代碼。
  • 增量編譯谒麦。熱重載模塊會(huì)將發(fā)生變化的 Dart 代碼俄讹,通過編譯轉(zhuǎn)化為增量的 Dart Kernel 文件。
  • 推送更新绕德。熱重載模塊將增量的 Dart Kernel 文件通過 HTTP 端口患膛,發(fā)送給正在移動(dòng)設(shè)備上運(yùn)行的 Dart VM。
  • 代碼合并耻蛇。Dart VM 會(huì)將收到的增量 Dart Kernel 文件踪蹬,與原有的 Dart Kernel 文件進(jìn)行合并,然后重新加載新的Dart Kernel 文件臣咖。
  • Widget 重建跃捣。在確認(rèn) Dart VM 資源加載成功后,F(xiàn)lutter 會(huì)將其 UI 線程重置夺蛇,通知 Flutter Framework 重建 Widget疚漆。

可以看到,F(xiàn)lutter 提供的熱重載在收到代碼變更后蚊惯,并不會(huì)讓 App 重新啟動(dòng)執(zhí)行愿卸,而只會(huì)觸發(fā) Widget 樹的重新繪制,因此可以保持改動(dòng)前的狀態(tài)截型,這就大大節(jié)省了調(diào)試復(fù)雜交互界面的時(shí)間趴荸。

比如,我們需要為一個(gè)視圖棧很深的頁面調(diào)整 UI 樣式宦焦,若采用重新編譯的方式发钝,不僅需要漫長的全量編譯時(shí)間,而為了恢復(fù)視圖棧波闹,也需要重復(fù)之前的多次點(diǎn)擊交互酝豪,才能重新進(jìn)入到這個(gè)頁面查看改動(dòng)效果。但如果是采用熱重載的方式精堕,不僅沒有編譯時(shí)間孵淘,而且頁面的視圖棧狀態(tài)也得以保留,完成熱重載之后馬上就可以預(yù)覽 UI 效果了歹篓,相當(dāng)于進(jìn)行了局部界面刷新瘫证。

不支持熱重載的場(chǎng)景

Flutter 提供的亞秒級(jí)熱重載一直是開發(fā)者的調(diào)試?yán)鳌Mㄟ^熱重載庄撮,我們可以快速修改 UI背捌、修復(fù) Bug,無需重啟應(yīng)用即可看到改動(dòng)效果洞斯,從而大大提升了 UI 調(diào)試效率毡庆。

不過,F(xiàn)lutter 的熱重載也有一定的局限性。因?yàn)樯婕暗綘顟B(tài)保存與恢復(fù)么抗,所以并不是所有的代碼改動(dòng)都可以通過熱重載來更新毅否。以下是Flutter開發(fā)中幾個(gè)不支持熱重載的典型場(chǎng)景:

  • 代碼出現(xiàn)編譯錯(cuò)誤;
  • Widget 狀態(tài)無法兼容乖坠;
  • 全局變量和靜態(tài)屬性的更改搀突;
  • main 方法里的更改;
  • initState 方法里的更改熊泵;
  • 枚舉和泛類型更改仰迁。

我們就具體看看這幾種場(chǎng)景的問題,應(yīng)該如何解決吧顽分!

代碼出現(xiàn)編譯錯(cuò)誤

當(dāng)代碼更改導(dǎo)致編譯錯(cuò)誤時(shí)徐许,熱重載會(huì)提示編譯錯(cuò)誤信息。比如下面的例子中卒蘸,代碼中漏寫了一個(gè)反括號(hào)雌隅,在使用熱重載時(shí),編譯器直接報(bào)錯(cuò)缸沃,如下所示恰起。

Initializing hot reload...
Syncing files to device iPhone X...

Compiler message:
lib/main.dart:84:23: Error: Can't find ')' to match '('.
    return MaterialApp(
                      ^
Reloaded 1 of 462 libraries in 301ms.

在這種情況下,只需更正上述代碼中的錯(cuò)誤趾牧,就可以繼續(xù)使用熱重載检盼。

Widget 狀態(tài)無法兼容

當(dāng)代碼更改會(huì)影響 Widget 的狀態(tài)時(shí),會(huì)使得熱重載前后 Widget 所使用的數(shù)據(jù)不一致翘单,即應(yīng)用程序保留的狀態(tài)與新的更改不兼容吨枉。

這時(shí),熱重載也是無法使用的哄芜。比如下面的代碼中貌亭,我們將某個(gè)類的定義從 StatelessWidget 改為 StatefulWidget 時(shí),熱重載就會(huì)直接報(bào)錯(cuò)认臊,如下所示圃庭。

//改動(dòng)前
class MyWidget extends StatelessWidget {
  Widget build(BuildContext context) {
    return GestureDetector(onTap: () => print('T'));
  }
}

//改動(dòng)后
class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> { /*...*/ }

當(dāng)遇到這種情況時(shí),我們需要重啟應(yīng)用失晴,才能看到更新后的程序的運(yùn)行效果冤议。

全局變量和靜態(tài)屬性的更改

在 Flutter 中,全局變量和靜態(tài)屬性都被視為狀態(tài)师坎,在第一次運(yùn)行應(yīng)用程序時(shí),會(huì)將它們的值設(shè)為初始化語句的執(zhí)行結(jié)果堪滨,因此在熱重載期間不會(huì)重新初始化胯陋。

比如下面的代碼中,我們修改了一個(gè)靜態(tài) Text 數(shù)組的初始化元素。雖然熱重載并不會(huì)報(bào)錯(cuò)遏乔,但由于靜態(tài)變量并不會(huì)在熱重載之后初始化义矛,因此這個(gè)改變并不會(huì)產(chǎn)生效果,代碼如下盟萨。

//改動(dòng)前
final sampleText = [
  Text("T1"),
  Text("T2"),
  Text("T3"),
  Text("T4"),
];

//改動(dòng)后
final sampleText = [
  Text("T1"),
  Text("T2"),
  Text("T3"),
  Text("T10"),    //改動(dòng)點(diǎn)
];

如果需要更改全局變量和靜態(tài)屬性的初始化語句凉翻,需要重啟應(yīng)用才能查看更改效果。

main 方法里代碼更改

在 Flutter 中捻激,由于熱重載之后只會(huì)根據(jù)原來的根節(jié)點(diǎn)重新創(chuàng)建控件樹制轰,因此 main 函數(shù)的任何改動(dòng)并不會(huì)在熱重載后重新執(zhí)行。所以胞谭,如果我們改動(dòng)了 main 函數(shù)體內(nèi)的代碼垃杖,是無法通過熱重載看到更新效果的。

//更新前
class MyAPP extends StatelessWidget {
@override
  Widget build(BuildContext context) {
    return const Center(child: Text('Hello World', textDirection: TextDirection.ltr));
  }
}

void main() => runApp(new MyAPP());

//更新后
void main() => runApp(const Center(child: Text('Hello, 2019', textDirection: TextDirection.ltr)));

由于 main 函數(shù)并不會(huì)在熱重載后重新執(zhí)行丈屹,因此以上改動(dòng)是無法通過熱重載查看更新的调俘。

initState 方法里代碼更改

在熱重載時(shí),F(xiàn)lutter 會(huì)保存 Widget 的狀態(tài)旺垒,然后重建 Widget彩库。而 initState 方法是 Widget 狀態(tài)的初始化方法,這個(gè)方法里的更改會(huì)與狀態(tài)保存發(fā)生沖突先蒋,因此熱重載后不會(huì)產(chǎn)生效果骇钦。

例如,在下面的例子中鞭达,我們將計(jì)數(shù)器的初始值由 10 改為 100司忱,代碼如下:

//更改前
class _MyHomePageState extends State<MyHomePage> {
  int _counter;
  @override
  void initState() {
    _counter = 10;
    super.initState();
  }
  ...
}

//更改后
class _MyHomePageState extends State<MyHomePage> {
  int _counter;
  @override
  void initState() {
    _counter = 100;
    super.initState();
  }
  ...
}

由于這樣的改動(dòng)發(fā)生在 initState 方法中,因此無法通過熱重載查看更新畴蹭,我們需要重啟應(yīng)用坦仍,才能看到更改效果。

枚舉和泛型類型更改

在 Flutter 中叨襟,枚舉和泛型也被視為狀態(tài)繁扎,因此對(duì)它們的修改也不支持熱重載。

比如在下面的代碼中糊闽,我們將一個(gè)枚舉類型改為普通類梳玫,并為其增加了一個(gè)泛型參數(shù),代碼如下右犹。

//更改前
enum Color {
  red,
  green,
  blue
}

class C<U> {
  U u;
}

//更改后
class Color {
  Color(this.r, this.g, this.b);
  final int r;
  final int g;
  final int b;
}

class C<U, V> {
  U u;
  V v;
}

Hot Reload 與 Hot Restart

針對(duì)上面不能使用 Hot Reload 的情況提澎,就需要使用 Hot Restart。Hot Restart 可以完全重啟您的應(yīng)用程序念链,但卻不用結(jié)束調(diào)試會(huì)話盼忌。

對(duì)于Android Studio來說积糯, 執(zhí)行 Hot Restart無需 stop操作,再Run 一下谦纱,就是 Hot Restart看成。

對(duì)于VS Code 來說,打開命令面板跨嘉,輸入 Flutter: Hot Restart 或者 直接快捷鍵 Ctrl+F5川慌,就可以使用 Hot Restart。

總結(jié)

Flutter 的熱重載是基于 JIT 編譯模式的代碼增量同步祠乃。由于 JIT 屬于動(dòng)態(tài)編譯,能夠?qū)?Dart 代碼編譯成生成中間代碼跳纳,讓 Dart VM 在運(yùn)行時(shí)解釋執(zhí)行忍饰,因此可以通過動(dòng)態(tài)更新中間代碼實(shí)現(xiàn)增量同步。

熱重載的流程可以分為 5 步寺庄,包括:掃描工程改動(dòng)艾蓝、增量編譯、推送更新斗塘、代碼合并赢织、Widget 重建。Flutter 在接收到代碼變更后馍盟,并不會(huì)讓 App 重新啟動(dòng)執(zhí)行于置,而只會(huì)觸發(fā) Widget 樹的重新繪制,因此可以保持改動(dòng)前的狀態(tài)贞岭,大大縮短了從代碼修改到看到修改產(chǎn)生的變化之間所需要的時(shí)間八毯。

另一方面,由于涉及到狀態(tài)的保存與恢復(fù)瞄桨,涉及狀態(tài)兼容與狀態(tài)初始化的場(chǎng)景话速,熱重載是無法支持的,如改動(dòng)前后 Widget 狀態(tài)無法兼容芯侥、全局變量與靜態(tài)屬性的更改泊交、main 方法里的更改、initState 方法里的更改柱查、枚舉和泛型的更改等廓俭。

可以發(fā)現(xiàn),熱重載提高了調(diào)試 UI 的效率唉工,非常適合寫界面樣式這樣需要反復(fù)查看修改效果的場(chǎng)景研乒。但由于其狀態(tài)保存的機(jī)制所限,熱重載本身也有一些無法支持的邊界淋硝。

如果你在寫業(yè)務(wù)邏輯的時(shí)候告嘲,不小心碰到了熱重載無法支持的場(chǎng)景错维,也不需要進(jìn)行漫長的重新編譯加載等待,只要點(diǎn)擊位于工程面板左下角的熱重啟(Hot Restart)按鈕橄唬,就可以以秒級(jí)的速度進(jìn)行代碼重新編譯以及程序重啟了,同樣也很快参歹。

參考鏈接:

https://zhuanlan.zhihu.com/p/89870807

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末仰楚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子犬庇,更是在濱河造成了極大的恐慌僧界,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件臭挽,死亡現(xiàn)場(chǎng)離奇詭異捂襟,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)欢峰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門葬荷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人纽帖,你說我怎么就攤上這事“弥保” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵雕崩,是天一觀的道長融撞。 經(jīng)常有香客問我,道長懦铺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任趁窃,我火速辦了婚禮急前,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘刨摩。我一直安慰自己寺晌,他們只是感情好呻征,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布陆赋。 她就那樣靜靜地躺著,像睡著了一般攒岛。 火紅的嫁衣襯著肌膚如雪胞锰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天顺饮,我揣著相機(jī)與錄音誊册,去河邊找鬼。 笑死君旦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的金砍。 我是一名探鬼主播麦锯,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼鹅巍!你這毒婦竟也來了料祠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤敛苇,失蹤者是張志新(化名)和其女友劉穎顺呕,沒想到半個(gè)月后括饶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體来涨,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蹦掐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棍厂。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖浦马,靈堂內(nèi)的尸體忽然破棺而出张漂,到底是詐尸還是另有隱情,我是刑警寧澤航攒,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布漠畜,位于F島的核電站,受9級(jí)特大地震影響憔狞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瘾敢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一簇抵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧正压,春花似錦、人聲如沸拓劝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至厢洞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間躺翻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工踊淳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留陕靠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓垄开,卻偏偏與公主長得像税肪,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子寸认,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353