0%

【文章翻譯】Flutter Hot Reload

【文章內容使用 Gemini 1.5 Pro 自動產生】

Flutter 熱重載:原理與效能提升

Flutter 的熱重載功能非常棒,按下鍵盤上的 “r” 鍵,就能在幾秒鐘內看到變更的效果。在終端機(或 IDE 底部)您可能會看到類似的訊息:Reloaded 1 of 553 libraries in 297ms。但熱重載背後的運作原理是什麼?Dart 和 Flutter 團隊又是如何讓它變得更快呢?

熱重載概述

Flutter 的熱重載主要包含以下五個步驟:

  1. flutter_tools 掃描需要的檔案變更。 它會檢查每個必要的檔案,並確認它的最後修改時間戳記是否比之前的編譯時間戳記更新。
  2. flutter_tools 指示正在執行的 Dart 編譯器重新編譯應用程式,並告知它哪些檔案已變更。 Dart 編譯器開始重新編譯。
  3. flutter_tools 將更新的檔案傳送到設備。 這包括任何變更的資產和新編譯的 _delta kernel 檔案_(編譯的輸出,Dart VM 可以理解的檔案)。
  4. flutter_tools 要求設備上的 Dart VM 中的所有隔離區重新載入它們的來源(讀取變更的 delta kernel 檔案並執行其魔力)。
  5. flutter_tools 指示設備上的應用程式重新組合 - 重新建立螢幕上的所有 Widget、重新載入資產、重新執行佈局、重新繪製等等。

在我的開發者機器上,目標設備為 Linux(也就是本地執行的桌面應用程式),在只更新 `lib/main.dart` 檔案的時間戳記後,在一個新建立的 `flutter create` 應用程式上執行第一次熱重載,我得到了以下時序(從 `flutter run -v` 中提取):

  1. 掃描檔案需要 ~13 ms。
  2. 重新編譯需要 ~67 ms。
  3. 將檔案傳送到設備需要 ~2 ms。
  4. Dart VM 重新載入來源需要 ~96 ms。
  5. 重新組合需要 ~114 ms。

如果我改用一個更大的應用程式(並變更其他檔案),我可能會得到以下時序:

  1. 掃描檔案需要 ~12 ms。
  2. 重新編譯需要 ~386 ms。
  3. 將檔案傳送到設備需要 ~2 ms。
  4. Dart VM 重新載入來源需要 ~171 ms。
  5. 重新組合需要 ~229 ms。

在兩種情況下,以下步驟佔用了最多的時間:

  • 重新編譯
  • 重新載入
  • 重新組合

要使熱重載速度更快,我們必須讓這三個步驟中的其中一個或多個步驟變得更快。 這裡我將重點關注第一部分:將變更的原始碼檔案重新編譯成 Dart VM 可以使用的內容。

重新編譯

從邏輯上來說,如果我作為使用者變更了一個檔案,例如 `foo.dart`,我可能會期望重新編譯看起來像這樣:

  1. 編譯器在記憶體中保留了舊的狀態。
  2. 編譯器被告知 foo.dart 已變更。
  3. 編譯器丟棄它對 foo.dart 的內部狀態。
  4. 編譯器重新編譯 foo.dart
  5. 完成。

這將非常棒。這意味著,無論我變更了哪個檔案,我只會重新編譯那個檔案,並且(可能)重新編譯速度很快。

不幸的是,重新編譯通常不會像這樣運作。以下是有兩個例子說明了為什麼重新編譯可能沒有那麼簡單:

  • foo.dart 曾經包含類別 Foo,該類別在各處使用。變更後的檔案不包含此類別(也許它被手動重新命名),並且每個使用此類別的檔案都應該出現編譯錯誤。
  • foo.dart 曾經定義了一個字段為 var z = 42。另一個檔案使用此字段:var z2 = z * 2。Dart 類型推斷確定 z 是一個整數,而 z2 是一個整數,因為 z 是一個整數。現在,字段變更為 var z = 42.2。這次 Dart 類型推斷將會確定該字段是一個雙精度數,但如果不重新編譯另一個函式庫,那么 z2 將仍然(錯誤地)被標記為一個整數。

因此,Dart 中的重新編譯長期以來都是像這樣執行的:

  1. 編譯器在記憶體中保留了舊的狀態。
  2. 編譯器被告知 foo.dart 已變更。
  3. 編譯器丟棄它對 foo.dart 的內部狀態。
  4. 編譯器檢查哪些檔案匯入或匯出 foo.dart,並將這些檔案也丟棄。
  5. 編譯器檢查哪些檔案匯入或匯出步驟 4 中的檔案,並將這些檔案也丟棄。
  6. 不斷重複:丟棄所有傳遞性匯入者和匯出者。
  7. 編譯器重新編譯所有(現在)“遺漏”的函式庫。
  8. 完成。

這聽起來可能很糟糕,但在許多情況下並非如此。雖然變更您自己的自訂 Widget 組合可能會導致重新編譯您編寫的所有程式碼,但它不會導致重新編譯 Flutter 框架本身,例如,因為 Flutter 框架不匯入或匯出您的函式庫。另一方面,如果您變更了 Flutter 框架的核心檔案,那麼您最終將重新編譯(幾乎)所有內容。

回顧一下為什麼只重新編譯單一變更檔案不起作用的原因的不完整列表,我們可能會看到一個模式:它不起作用是因為您進行了 _全局_ 變更 - 影響其他函式庫的變更。但是,如果您只變更了註釋呢?或者在您的建構方法中加入了另一個除錯列印?或者修正了實用方法中的一處錯誤?這些變更不是全局的,我們應該可以做得更好!

改進

對於非全局變更(不能影響其他函式庫編譯的變更),我們實際上可以只重新編譯變更的函式庫,並且仍然保持語義。主要問題是確定何時變更是全局的,何時不是(並且要快速完成)。幸運的是,這可以通過增量步驟來完成:我們不必立即(或根本不必)使其完美。

第一步可能是比較檔案的舊版本和新版本,同時忽略兩個版本的檔案中的註釋。如果,用這種方式比較時,兩個版本的檔案相同,那麼我們認為沒有全局變更,然後我們繼續重新編譯單一變更的檔案,而不是傳遞性匯入匯出圖。這種技術並不完美。例如,它仍然會在修正實用方法中的錯誤時觸發所有傳遞性匯入者和匯出者的重新編譯。但它允許您在只重新編譯一個檔案的同時修正註釋中的拼寫錯誤。

這裡快速說明一下:如果我們只變更了註釋,為什麼我們必須重新編譯?主要原因是堆疊追蹤。在內部,一些節點(表示您的程式碼)包含 _偏移量_ - 有關它們在檔案中的位置的資訊。如果此資訊過時,您的堆疊追蹤將包含無效的資訊。例如,它可能會聲稱某件事發生在第 42 行,而實際上並非如此。

若要達到可以實際上修正實用方法中的錯誤,同時仍然只重新編譯該檔案的狀態,我們必須在檢查全局變更時忽略另一件事:函數體。我們將再次比較變更檔案的之前版本和之後版本,這次同時忽略註釋和函數體。如果它們相同,我們將只重新編譯該檔案。

現在,我們實際上可以進行一些有用的變更,而無需重新編譯超出您變更的檔案的內容。您可以加入、移除或以其他方式變更註釋。您可以在您的建構方法中加入(和移除)除錯列印。您甚至可以修正實用方法中的錯誤。

好消息!

事實證明,這些對重新編譯的改進已經實現。如果您使用的是 Flutter 2.2,您可能已經注意到它了。如果沒有,您可能現在就會注意到。老實說,對於小型應用程式,您可能不會注意到多少速度提升,但對於大型應用程式,您應該會注意到。

我已經製作了一些非全局變更的示例,以評估其效果。

對於 [Veggie Seasons](https://github.com/flutter/samples/tree/master/veggieseasons) 示例應用程式(一個相對較小的應用程式):

  • 變更 lib/main.dart 沒有改善。它之前編譯一個檔案,現在仍然編譯一個檔案。
  • 變更 lib/data/veggie.dart 會帶來 30% 的改善。在我的電腦上,實際的編譯時間從 100 多毫秒下降到不到 20 毫秒(它以前編譯 18 個檔案,現在只編譯 1 個檔案)。這自然遠遠超過 30%,但由於重新編譯只是三個時間消耗中的其中一個(另外兩個是重新載入和重新組合),所以總體變更大約是 30%。

對於 [Flutter Gallery](https://github.com/flutter/gallery)(一個相對較大的應用程式):

  • 變更 lib/main.dart 會產生非常小的改善(它編譯 1 個檔案而不是 2 個檔案)。
  • 變更 lib/layout/adaptive.dart 會導致重新載入時間幾乎減半。僅重新編譯時間從近 400 毫秒下降到 40 毫秒(重新編譯 1 個檔案而不是 47 個檔案)。

您應該期望在實際情況中,Flutter 2.2 中的熱重載平均速度比 Flutter 2.0 中快 30%。從這個角度來看,這個變更為 Flutter 開發人員節省了超過一年時間,每 5 天就少等一次熱重載。

注意事項

我們對熱重載的變更並不總是意味著編譯器做的事情更少。例如,如果您加入或移除了一個方法,編譯器不會做的事情更少。如果您變更了字段的初始化器,編譯器不會做的事情更少。如果您變更了類別層級,編譯器不會做的事情更少。如果您變更了函數體(編譯器通常應該做的事情更少),由於混合和 FFI 方面的技術問題,編譯器可能仍然需要做同樣多的工作。

此外,當我們討論比較檔案時,我們跳過了幾個技術細節。首先,我們不能忽略 _所有_ 註釋:我們需要保留 [語言版本選擇器](https://dart.dev/guides/language/evolution#per-library-language-version-selection) `@dart version 標記` ,因為它具有語義意義。其次,我們不能忽略所有函數體,因為混合和 FFI 方面存在實作上的問題。

結語

Flutter 2.0 中的熱重載速度很快,但 Flutter 2.2 中的速度更快。平均而言,Flutter 2.2 中的熱重載速度比 Flutter 2.0 快約 30%,這為 Flutter 開發人員節省了超過一年時間,每 5 天就少等一次熱重載。

如果您尚未更新(甚至尚未嘗試使用 Flutter),現在可能是參觀 [flutter.dev](http://flutter.dev) 並嘗試一下的好時機。


Flutter 熱重載 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。