眾所周知,在Flutter 應用的Debug模式下,當我們開啟【Hot Reload】功能時,不需要在重啟應用即可看到最新的程式碼效果。這種類似於RN、Weex和小程式的熱載入功能是如何做到的呢,它背後的原理是什麼?
基本使用方法
Flutter的熱過載(hot reload)功能可以幫助您在無需重新啟動應用的情況下快速、輕鬆地進行測試、構建使用者介面、新增功能以及修復錯誤。 通過將更新後的原始碼檔案注入正在執行的Dart虛擬機器(VM)中來實現熱過載。在虛擬機器使用新的的欄位和函式更新類後,Flutter框架會自動重新構建widget樹,以便您快速檢視更改的效果。
我們編寫一個應用,執行應用程式,然後修改 Flutter APP 工程裡的 Dart 程式碼,然後點選【Hot Reload】按鈕開啟熱過載,如下圖所示。
VS Code 開啟 Hot Reload 。 當我們修改Dart程式碼,點選儲存的時候,就會看到介面已經發生了變化,如下圖。總結一下,在Flutter中使用熱過載需要經過以下幾個步驟:
- 連線真機或虛擬機器,執行 Flutter APP,且必須以 Debug 模式啟動。因為只有 Debug 模式才能使用 Hot Reload。
- 修改 Flutter APP 工程裡的 Dart 程式碼,但並不是所有 Dart 程式碼的修改都可以使用 Hot Reload,有一些情況下Hot Reload 並不能生效,只能使用 Hot Restart(重新啟動)。
- 使用快捷鍵 ctrl+s(Windows、Linux)或者 cmd+s(MacOS),或者點選 Hot Reload 的按鈕,就完成了Hot Reload 的操作,
Hot Reload 成功後,會在 Debug Consol 中看到輸出如下類似的訊息:
Performing hot reload...
Reloaded 1 of 448 libraries in 2,777ms.
複製程式碼
工作原理
熱過載
熱過載是指,在不中斷 App 正常執行的情況下,動態注入修改後的程式碼片段。而這一切的背後,離不開 Flutter 所提供的執行時編譯能力。為了更好地理解 Flutter 的熱過載實現原理,我們先簡單回顧一下 Flutter 編譯模式背後的技術吧。
JIT
JIT(Just In Time),指的是即時編譯或執行時編譯,在 Debug 模式中使用,可以動態下發和執行程式碼,啟動速度快,但執行效能受執行時編譯影響。
AOT
AOT(Ahead Of Time),指的是提前編譯或執行前編譯,在 Release 模式中使用,可以為特定的平臺生成穩定的二進位制程式碼,執行效能好、執行速度快,但每次執行均需提前編譯,開發除錯效率低。
可以看到,Flutter 提供的兩種編譯模式中,AOT 是靜態編譯,即編譯成裝置可直接執行的二進位制碼;而 JIT 則是動態編譯,即將 Dart 程式碼編譯成中間程式碼(Script Snapshot),在執行時裝置需要 Dart VM 解釋執行。
而熱過載之所以只能在 Debug 模式下使用,是因為 Debug 模式下,Flutter 採用的是 JIT 動態編譯(而 Release 模式下采用的是 AOT 靜態編譯)。JIT 編譯器將 Dart 程式碼編譯成可以執行在 Dart VM 上的 Dart Kernel,而 Dart Kernel 是可以動態更新的,這就實現了程式碼的實時更新功能,原理如下圖。
總體來說,完成熱過載的可以分為掃描工程改動、增量編譯、推送更新、程式碼合併、Widget 重建 5 個步驟。
- 工程改動。熱過載模組會逐一掃描工程中的檔案,檢查是否有新增、刪除或者改動,直到找到在上次編譯之後,發生變化的 Dart 程式碼。
- **增量編譯。**熱過載模組會將發生變化的 Dart 程式碼,通過編譯轉化為增量的 Dart Kernel 檔案。
- 推送更新。熱過載模組將增量的 Dart Kernel 檔案通過 HTTP 埠,傳送給正在移動裝置上執行的 Dart VM。
- 程式碼合併。Dart VM 會將收到的增量 Dart Kernel 檔案,與原有的 Dart Kernel 檔案進行合併,然後重新載入新的Dart Kernel 檔案。
- Widget 重建。在確認 Dart VM 資源載入成功後,Flutter 會將其 UI 執行緒重置,通知 Flutter Framework 重建 Widget。
可以看到,Flutter 提供的熱過載在收到程式碼變更後,並不會讓 App 重新啟動執行,而只會觸發 Widget 樹的重新繪製,因此可以保持改動前的狀態,這就大大節省了除錯複雜互動介面的時間。
比如,我們需要為一個檢視棧很深的頁面調整 UI 樣式,若採用重新編譯的方式,不僅需要漫長的全量編譯時間,而為了恢復檢視棧,也需要重複之前的多次點選互動,才能重新進入到這個頁面檢視改動效果。但如果是採用熱過載的方式,不僅沒有編譯時間,而且頁面的檢視棧狀態也得以保留,完成熱過載之後馬上就可以預覽 UI 效果了,相當於進行了區域性介面重新整理。
不支援熱過載的場景
Flutter 提供的亞秒級熱過載一直是開發者的除錯利器。通過熱過載,我們可以快速修改 UI、修復 Bug,無需重啟應用即可看到改動效果,從而大大提升了 UI 除錯效率。
不過,Flutter 的熱過載也有一定的侷限性。因為涉及到狀態儲存與恢復,所以並不是所有的程式碼改動都可以通過熱過載來更新。以下是Flutter開發中幾個不支援熱過載的典型場景:
- 程式碼出現編譯錯誤;
- Widget 狀態無法相容;
- 全域性變數和靜態屬性的更改;
- main 方法裡的更改;
- initState 方法裡的更改;
- 列舉和泛型別更改。
我們就具體看看這幾種場景的問題,應該如何解決吧!
程式碼出現編譯錯誤
當程式碼更改導致編譯錯誤時,熱過載會提示編譯錯誤資訊。比如下面的例子中,程式碼中漏寫了一個反括號,在使用熱過載時,編譯器直接報錯,如下所示。
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.
複製程式碼
在這種情況下,只需更正上述程式碼中的錯誤,就可以繼續使用熱過載。
Widget 狀態無法相容
當程式碼更改會影響 Widget 的狀態時,會使得熱過載前後 Widget 所使用的資料不一致,即應用程式保留的狀態與新的更改不相容。
這時,熱過載也是無法使用的。比如下面的程式碼中,我們將某個類的定義從 StatelessWidget 改為 StatefulWidget 時,熱過載就會直接報錯,如下所示。
//改動前
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return GestureDetector(onTap: () => print('T'));
}
}
//改動後
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> { /*...*/ }
複製程式碼
當遇到這種情況時,我們需要重啟應用,才能看到更新後的程式的執行效果。
全域性變數和靜態屬性的更改
在 Flutter 中,全域性變數和靜態屬性都被視為狀態,在第一次執行應用程式時,會將它們的值設為初始化語句的執行結果,因此在熱過載期間不會重新初始化。
比如下面的程式碼中,我們修改了一個靜態 Text 陣列的初始化元素。雖然熱過載並不會報錯,但由於靜態變數並不會在熱過載之後初始化,因此這個改變並不會產生效果,程式碼如下。
//改動前
final sampleText = [
Text("T1"),
Text("T2"),
Text("T3"),
Text("T4"),
];
//改動後
final sampleText = [
Text("T1"),
Text("T2"),
Text("T3"),
Text("T10"), //改動點
];
複製程式碼
如果需要更改全域性變數和靜態屬性的初始化語句,需要重啟應用才能檢視更改效果。
main 方法裡程式碼更改
在 Flutter 中,由於熱過載之後只會根據原來的根節點重新建立控制元件樹,因此 main 函式的任何改動並不會在熱過載後重新執行。所以,如果我們改動了 main 函式體內的程式碼,是無法通過熱過載看到更新效果的。
//更新前
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 函式並不會在熱過載後重新執行,因此以上改動是無法通過熱過載檢視更新的。
initState 方法裡程式碼更改
在熱過載時,Flutter 會儲存 Widget 的狀態,然後重建 Widget。而 initState 方法是 Widget 狀態的初始化方法,這個方法裡的更改會與狀態儲存發生衝突,因此熱過載後不會產生效果。
例如,在下面的例子中,我們將計數器的初始值由 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();
}
...
}
複製程式碼
由於這樣的改動發生在 initState 方法中,因此無法通過熱過載檢視更新,我們需要重啟應用,才能看到更改效果。
列舉和泛型型別更改
在 Flutter 中,列舉和泛型也被視為狀態,因此對它們的修改也不支援熱過載。
比如在下面的程式碼中,我們將一個列舉型別改為普通類,併為其增加了一個泛型引數,程式碼如下。
//更改前
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
針對上面不能使用 Hot Reload 的情況,就需要使用 Hot Restart。Hot Restart 可以完全重啟您的應用程式,但卻不用結束除錯會話。
對於Android Studio來說, 執行 Hot Restart無需 stop操作,再Run 一下,就是 Hot Restart。
對於VS Code 來說,開啟命令皮膚,輸入 **Flutter: Hot Restart ** 或者 直接快捷鍵 Ctrl+F5,就可以使用 Hot Restart。
總結
Flutter 的熱過載是基於 JIT 編譯模式的程式碼增量同步。由於 JIT 屬於動態編譯,能夠將 Dart 程式碼編譯成生成中間程式碼,讓 Dart VM 在執行時解釋執行,因此可以通過動態更新中間程式碼實現增量同步。
熱過載的流程可以分為 5 步,包括:掃描工程改動、增量編譯、推送更新、程式碼合併、Widget 重建。Flutter 在接收到程式碼變更後,並不會讓 App 重新啟動執行,而只會觸發 Widget 樹的重新繪製,因此可以保持改動前的狀態,大大縮短了從程式碼修改到看到修改產生的變化之間所需要的時間。
另一方面,由於涉及到狀態的儲存與恢復,涉及狀態相容與狀態初始化的場景,熱過載是無法支援的,如改動前後 Widget 狀態無法相容、全域性變數與靜態屬性的更改、main 方法裡的更改、initState 方法裡的更改、列舉和泛型的更改等。
可以發現,熱過載提高了除錯 UI 的效率,非常適合寫介面樣式這樣需要反覆檢視修改效果的場景。但由於其狀態儲存的機制所限,熱過載本身也有一些無法支援的邊界。
如果你在寫業務邏輯的時候,不小心碰到了熱過載無法支援的場景,也不需要進行漫長的重新編譯載入等待,只要點選位於工程皮膚左下角的熱重啟(Hot Restart)按鈕,就可以以秒級的速度進行程式碼重新編譯以及程式重啟了,同樣也很快。