眾所周知簇宽,在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】按鈕開啟熱重載,如下圖所示诗鸭。
VS Code 開啟 Hot Reload 染簇。
當(dāng)我們修改Dart代碼,點(diǎn)擊保存的時(shí)候强岸,就會(huì)看到界面已經(jīng)發(fā)生了變化锻弓,如下圖。
總結(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í)編譯影響滩褥。
AOT
AOT(Ahead Of Time),指的是提前編譯或運(yùn)行前編譯炫加,在 Release 模式中使用瑰煎,可以為特定的平臺(tái)生成穩(wěn)定的二進(jìn)制代碼,執(zhí)行性能好俗孝、運(yùn)行速度快酒甸,但每次執(zhí)行均需提前編譯,開發(fā)調(diào)試效率低赋铝。
可以看到插勤,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í)更新功能嚎货,原理如下圖。
總體來說蔫浆,完成熱重載的可以分為掃描工程改動(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)行代碼重新編譯以及程序重啟了,同樣也很快参歹。