【文章翻譯】GSoC ’21: Creating a desktop sample for Flutter

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

GSoC ‘21:為 Flutter 建立桌面樣本

Google Summer of Code (GSoC) 是一個由 Google 贊助的開源夏季程式。在此程式期間,學生在開源組織的指導下進行專案。

這是 Dart 團隊第二次作為 GSoC 的指導組織參與。不幸的是,由於指導老師的可用性問題,今年的 點子列表 上沒有任何 Flutter 專案。由於這是我的第二次參與 GSoC,我聯絡了去年的指導老師 Brett,看看是否有機會參與一個專案。在與他以及組織管理員討論後,我找到了一個可以參與的專案。

此部落格展示了我為專案所做的工作。查看今年 Dart 下的所有專案。所有產生的原始碼都位於 GitHub 儲存庫中,您可以透過查看個別的拉取請求 (PR) 來查看工作的進展。

專案詳情:Flutter 桌面工具

今年,在 Flutter Engage 中,Flutter 桌面支援的 beta 快照已包含在穩定版本中。這導致需要為 samples 儲存庫建立一個桌面樣本,該樣本日後可以在桌面應用程式商店(如 macOS App StoreMicrosoft StoreLinux Snap Store)上發佈。在與 Brett 和團隊討論後,我們決定建立一個桌面樣本,它也將是一個工具,可以幫助開發人員管理其專案的 linting 規則。請查看 GitHub 上的 linting_tool

應用程式的螢幕截圖

該應用程式使用 Dart 團隊從 dart-lang/linter 儲存庫中託管的 linting 規則,並且該應用程式可以執行以下操作:

顯示可用的 Dart linting 規則列表。

您可以從 API 獲取 linting 規則列表。該應用程式使用 flutter_markdown 套件來格式化詳細資訊和程式碼片段。

相關 PR:#856

將規則儲存到不同的設定檔。

您可以為不同類型的專案建立不同的規則設定檔。該應用程式使用 hive 資料庫來維持持久性儲存。

已儲存設定檔的列表

相關 PR:#860

修改和匯出設定檔。

設定檔可以根據您的喜好進行修改,然後匯出到 Flutter 專案中,作為 analysis_options.yaml 檔案。 套件 yamljson2yamlfile_selector 用於完成此操作。

相關 PR:#874#869

顯示預設設定檔。

您可以查看 effective_dartpedanticlintsflutter_lints 使用的預設設定檔列表。

相關 PR:**#871**

若要查看專案在 GSoC 期間的進展,請查看 這組 PR

我還想為專案添加一些其他內容。兩項主要内容是:能夠從現有的 analysis_options.yaml 檔案載入設定檔,以及能夠在規則列表中搜索特定規則。我還正在製作一個新的影片系列,將涵蓋如何部署 Flutter 桌面應用程式。影片系列製作完成後,我會更新此部落格,也會在 Twitter 帳戶 上分享。

經驗

我這次的 Google Summer of Code (GSoC) 經驗比去年更好。(查看 Learn testing with the new Flutter sample 以閱讀更多有關我去年參與的專案的資訊。)我認為今年的經驗更好,因為專案更具技術性,涵蓋了更多概念,並且與我之前從未參與過的內容相關。這次我也認識團隊中的更多人。我認為 GSoC 將會是我永遠不會忘記的人生的一部分。

指導

Brett 一直都在,幫助我解決所有問題和阻礙。當我需要更多時間來處理其他承諾(如學校、其他工作和家庭相關事項)時,他非常理解。我認為最棒的是,他給了我完全的自由去探索事物,如果我認為某些內容對專案來說是寶貴的補充,我就可以調整專案的範圍。這次我做了一件很酷的事情,就是在每次每周會議結束時,我問他一些與專案無直接關聯的一般軟體工程相關的問題,例如「Google 如何處理估計?」和「你對遠端工作怎麼看?」。這幫助我學習了一些與程式設計無關的軟體產業知識。我強烈建議你在實習或 GSoC 類專案期間嘗試一下。而且,是的,他仍然是我合作過的最酷的人。

學習

學習是開源軟體最棒的部分。我這次做的事情,很多都無法透過 Google 搜尋找到答案。我閱讀了大量的程式碼,找出不同的套件如何執行相同的事情,以及它們之間的差異模式。我還學會了如何在大量可用的套件中為專案找到合適的套件。由於這個類別對我來說是全新的,因此這次的經驗幫助我在短時間內學到很多東西。

挑戰

我這次遇到了一些挑戰,從中學到了一些經驗。第一個挑戰與時間表有關。今年,GSoC 的時間表縮短了一半,這迫使我們重新排序優先順序,跳過許多可能會成為專案中寶貴補充的功能。其他挑戰是:處理一個全新的類別,以及在一個不太流行的類別中,找到關於如何執行某些事情的良好資源。

結語

在過去的一年中,我與 Flutter 和 samples 儲存庫合作,獲得了難以置信的經驗。我想要感謝 Brett、Flutter 團隊以及 GSoC 團隊。

關於作者:Abdullah 是一位來自印度浦那的電腦工程應屆畢業生。他在過去 4 年中一直在開發行動應用程式,並且喜歡與行動應用程式相關的內容合作。您可以在 TwitterLinkedInGitHub 上聯繫他.


GSoC ‘21:為 Flutter 建立桌面樣本 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

http://creativecommons.org/licenses/by/4.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,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】What can we do better to improve Flutter? — Q2 2021 user survey results

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

我們如何才能讓 Flutter 變得更好?- 2021 年第二季使用者調查結果

撰寫者: JaYoungMariam

Flutter 團隊每季都會進行使用者調查,以聽取 Flutter 使用者的意見並為未來做出規劃。最近一次的調查是在 5 月份進行的,這是我們第 13 次使用者調查,收集了超過 7,000 份回覆。我們請 Flutter 開發人員評估他們對 Flutter 各部分的滿意度,對於那些不完全滿意的開發人員,我們請他們說明原因。團隊不僅總結了多選題調查回覆,還閱讀了調查中留下的開放式意見。總共有 3,403 則意見。我們再次回來分享我們從您那裡學到的東西。

滿意度

總體而言,92% 的受訪者表示他們對 Flutter 持正面滿意度(39% 有點滿意,53% 非常滿意)。Flutter 的子系統滿意度得分不同,範圍從 72% 到 93%,如下圖所示。雖然我們很高興分享我們正在維持高滿意度,但我們也關注可以改進的特定領域。

開放式問題

調查中有兩個開放式問題:

  • 「您為什麼對 Flutter 不滿意?請分享您的原因:」
  • 「您想對 Flutter 團隊說些什麼嗎? 」

我們從第一個問題收集了 847 則意見,從第二個問題收集了 2,556 則意見。這些意見首先使用機器學習演算法被劃分為各種主題(例如文件、生態系統、Web 支援等)。然後,每個主題會被分配給在該領域工作的團隊進行分析。

當然,團隊不能一次承擔所有工作並解決所有問題。然而,每個團隊都試圖從意見中提取見解並優先處理想法,仔細評估需要達成的效果和付出的努力。在以下章節中,我們將分享一些我們得到的結論的範例。

範例 1:關於一般的開發者體驗

在高層面上,透過閱讀書面回饋對我們有三個幫助:

  1. 確認現有問題
  2. 找出新的關注領域和想法
  3. 察覺到對額外資訊和證據的需求

首先,我們確認了在許多其他來源(例如 GitHub Issues 標籤、Reddit 和 Stack Overflow)中經常觀察到的痛點。這些範例包括但不限於記憶體消耗和升級技能的範例。我們非常了解這些挑戰,並在每個小組內為它們設定優先順序。同時,我們希望再次宣傳現有的資源,例如 使用記憶體檢視學習中心

其次,我們發現了一些令我們驚訝的主題。例如,有幾則意見表達了對擴展到桌面和 Web 的擔憂。這些意見要求我們專注於讓 iOS 和 Android 變得更好,不要分散我們的注意力。我們理解這種擔憂,並且會注意這個問題。我們也會更好地傳達我們如何努力成為一種強大的行動技術,以及隨著時間的推移如何擴展對其他平台的支援。

最後,我們希望獲得有關調查中報告的一些問題的更多資訊,特別是那些與效能和文件相關的問題。如果您想與 Flutter 貢獻者進行對話,並為您的問題添加更多細節,請考慮將它們發佈到 GitHub Issues 標籤。這個標籤會定期由團隊進行分類。在您發佈時,請遵循 為所有問題提交錯誤 中的規則。(當然,在 Stack Overflow 或 Discord 等支援論壇中回答「如何」類別的問題效果會更好!請查看 社群 頁面以獲取更多資訊。)

範例 2:關於 Flutter 的 Web 支援

團隊發現,圍繞缺乏穩定版本 Web 支援而提出的不滿意回饋,在 Flutter 2.0 於 3 月份 穩定發佈 Web 支援 之後已得到緩解。我們為 Flutter Web 支援的穩定版本新增的許多功能都是基於去年的 調查,在那裡我們詢問了您可能需要的 Web 應用程式功能。

2020 年第三季調查 中的 Web 特定問題是基於我們去年第二季調查中收到的開放式回饋。您的意見幫助我們確定了我們應該優先考慮的領域,以作為 Flutter Web 支援的初始穩定版本。

在 2020 年,15% 的意見是關於效能和卡頓的,因此我們優先改進 HTML 渲染器的效能,以及穩定 CanvasKit 渲染器。另外 15% 的意見是關於我們的 Plugin 差距,以及對 GoogleMaps、Firestore 等等的要求,因此我們確保了在 Web 穩定版本中支援了大多數 Google 自有 Plugin,包括 GoogleMaps、Firebase_analytics、cloud_firestore 等等。有幾則意見是關於捲軸和文字渲染的。雖然我們在這些領域仍然需要做更多工作,但我們能夠為支援 桌面外觀尺寸上的捲軸 以及支援 富文本功能(例如文字欄位中的多行文字選取)奠定堅實的基礎。

本季,我們得知使用者認為 Flutter 的 Web 支援仍然需要改進。Web 應用程式的「外觀和感覺」是最常被提及的不滿意 Flutter Web 支援的原因。我們也被要求提供更多 Web 特定 Widget、直觀的響應式 Widget 等等。搜尋引擎優化 (SEO) 的要求也被注意到。其他的不滿意原因是從右到左 (RTL) 文字支援、程式碼大小、路由和除錯。

我們計劃在下一次使用者調查中詢問一些這些領域,因為我們正在規劃下一個版本。對於其他的問題,我們已經取得了進展,例如新增 RTL 文字支援,透過我們目前的 UXR 研究 了解路由問題,以及研究降低程式碼大小的方法。

接下來要做什麼?

每季調查計劃是讓我們更好地了解您的需求的結構化方式,但它不是我們聽取 Flutter 開發者意見的唯一管道。如果您有需要後續處理的緊急問題,請在 GitHub 上提交它們。

Flutter UXR 團隊將繼續透過 flutter.dev 上的公告、Flutter IDE Plugin 或 Twitter @FlutterDev 每季進行調查。請繼續提供您的想法,因為團隊正在尋找重要問題的答案。您也可以透過 註冊即將到來的 UXR 研究 來參與其他研究。

再次感謝所有參與本次調查並提供寶貴意見的各位。我們的目標是建立您喜歡的產品,我們感謝您撥冗幫助我們。


我們如何才能讓 Flutter 變得更好?- 2021 年第二季使用者調查結果 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】Adding Flutter to your existing iOS and Android codebases

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

將 Flutter 加入現有的 iOS 和 Android 程式碼庫

無論您是為小型代理商工作,為眾多客戶建立行動應用程式,還是為擁有數百個內部應用程式的龐大企業開發,支援多個程式碼庫都可能很困難且昂貴(如果真的要做到)。我們驚訝地發現,一個常見的場景是,一些公司有數十甚至數百個應用程式是用於一個主要的行動平台,但沒有為另一個平台開發。這會讓他們許多使用者(通常是員工)無法像其他人一樣高效地執行相同的任務,從而可能導致價值流失。

Flutter 是一個可以從單一程式碼庫支援多個平台的 UI 架構,它可以幫助解決這些問題。雖然在理想情況下,您可以使用 Flutter 從頭開始建立應用程式,但當公司已經投入時間和金錢開發一個或多個平台的應用程式時,這個計劃通常行不通。

在本教程中,我們將透過學習稱為 add-to-app 的功能,探討一種更務實的方法,即隨著時間推移逐步將現有的 iOS 或 Android 應用程式轉換為 Flutter。雖然此功能並不能立即為您提供完整的 Flutter 應用程式,但它允許您在轉換過程中保持與當前程式碼庫的功能一致性和穩定性,而不是需要完整的重構,而重構可能會充滿意想不到的問題和「陷阱」。

在我們逐步完成本教程時,我們將從一個簡單的基本案例應用程式開始,與您期望從「你好,世界!」範例中得到的結果類似,適用於 iOS 和 Android,而不是加入已經存在的實際應用程式的複雜性。之後,我們將建立將為每個平台新增新視圖(用 Flutter 編寫)所需的基礎架構。在實作 add-to-app 時,預期您已經具備一些 Flutter 的經驗,但本教程盡可能保持簡單,以便專注於如何將 Flutter 加入非 Flutter 應用程式。透過這種方式,您最終將擁有必要的詞彙,並知道在您準備好自己嘗試時該去哪裡尋找。

讓我們開始進行吧!

Flutter 安裝

如果您尚未在電腦上安裝 Flutter SDK,現在正是時候。請按照 此連結 中的說明來設定您的機器。我會在這裡等您:) 如果你想在沒有跟著做的情況下繼續閱讀,那也是完全可以的。

都準備好了嗎?太棒了!

因此,將 Flutter 加入現有應用程式的第一步是,毫不意外地,建立一個要加入應用程式的 Flutter 元件。從命令列介面,導航到您想要儲存 Flutter 模組的目錄,並使用 Flutter CLI 工具執行以下命令:

1
flutter create --template module add_to_app_flutter_module

這會建立一個基本 Flutter 應用程式並將其放置在一個名為 add_to_app_flutter_module 的新目錄中,不過您可以隨意為模組命名 - 本教程假設您使用的是 add_to_app_flutter_module 名稱。

iOS 安裝

通常,在實作 add-to-app 功能時,您已經有現有的 iOS 或 Android 應用程式。在本教程中,您將從頭開始建立新的應用程式,以便專注於實作基礎。您將從 Xcode 中建立一個全新的 iOS 應用程式開始。如果您沒有使用 Mac 電腦或為 iOS 開發,請隨時跳到 Android 部分,或繼續閱讀以了解此過程。我只會有點失望您沒有閱讀全部內容。啟動 Xcode。當出現第一個選項螢幕時,選擇 App 並點擊 Next

在下一頁,適當地填寫文字欄位。在本教程中,使用 Storyboard 介面、Swift 作為語言,以及 UIKit App Delegate 作為生命週期。

此時,系統會提示您在電腦上的某個位置建立一個新的目錄,並將您的應用程式放置到其中。在本教程中,將新的 iOS 專案資料夾儲存在與您之前建立的 Flutter 模組相同的父目錄中。建立該目錄後,您將進入 Xcode 專案螢幕,資料夾結構類似於此:

回到命令列,導航到您在上一步中建立的新的 iOS 專案目錄,並使用以下命令初始化 CocoaPods:

1
pod init

初始化 Podfile 後,從 CLI 打開它,並將其內容替換為以下內容(請記住將目標名稱從 Add-to-App 更改為反映您自己的應用程式名稱,並將 flutter_application_path 更改為與 Flutter 模組的路徑匹配,如果您使用的是不同的值):

更新 Podfile 後,儲存檔案並執行以下命令將 Flutter 模組連結到新的 iOS 專案:

1
pod install

從 iOS 應用程式打開預設 Flutter 頁面

現在 Flutter 模組和 iOS 專案已連結,是時候學習如何從 iOS ViewController 導航到行動應用程式中的 Flutter 頁面了。首先打開 AppDelegate.swift 檔案,將類別設定為繼承 FlutterAppDelegate 而不是預設的 UIAppDelegate。您還想定義一個新的 FlutterEngine 物件,它是用於橋接原生 iOS 應用程式和 Flutter 類別的 Flutter 環境容器:

要完成 simpleFlutterAppDelegate 類別,請建立一個新的應用程式函數,在啟動 iOS 應用程式時註冊 FlutterEngine:

在 AppDelegate 類別中,您只需要執行這些操作(暫時如此)。要啟動預設的 Flutter 螢幕,請轉到專案的 ViewController.swift 檔案(不過,在更完善的應用程式中,您可以使用任何 ViewController)。新增一個名為 showFlutter() 的函數,該函數會擷取 FlutterEngine,並使用預設的 Flutter 輸入點建立一個新的 FlutterViewController 物件,然後顯示它:

接下來,您需要一種方法來呼叫該函數。為簡單起見,直接在 Swift 程式碼中為螢幕定義一個按鈕,不過您可以使用任何適合您的其他導航模式或技術。在本教程中,建立一個新的 UIButton,將其置於螢幕中間,將新的 showFlutter() 函數指定給按鈕的動作,然後將其附加到視圖,所有這些都來自 viewDidLoad() 生命週期函數:

現在嘗試執行應用程式。如果一切按預期進行(祈禱吧!),那麼您應該能夠啟動 iOS 應用程式,點擊 顯示 Flutter! 按鈕,然後觀看新的 Flutter 螢幕彈出:

Android 安裝

現在您已經使 iOS/Flutter 組合正常運作,是時候嘗試設定 Android 應用程式了。就像您對 iOS 專案所做的那樣,建立一個新的 Android 專案,並在第一個螢幕上選擇 基本活動 模板。

在下一頁,為名稱和套件名稱填寫適當的資訊。為了使一切與此範例的 iOS 版本保持一致,請將您的專案儲存在與 Flutter 模組和 iOS 應用程式相同的父目錄下。您還需要確保在本教程中將專案的語言設定為 Kotlin,儘管相同 add-to-app 邏輯適用於使用 Java 編寫的 Android 應用程式。準備好後,點擊藍色的 完成 按鈕。

現在您有了基礎 Android 專案,新增您之前建立的 Flutter 模組。您可以透過轉到 檔案 -> 新建 -> 新模組… 來完成此操作。

從那裡,轉到新視窗底部的 匯入 Flutter 模組 選項,加入 Flutter 模組位置,然後點擊藍色的 完成 按鈕。

接下來,打開 settings.gradle 檔案,並將其內容替換為以下內容:

這裡主要的部分是綁定並將 include_flutter.groovy 檔案加入到專案中。完成後,轉到專案級別的 build.gradle 檔案(位於 Android 專案的根目錄中),新增一個 allprojects 區塊,以便您可以編譯應用程式(這可能在以後不需要,但我遇到了 Android Studio Arctic Fox 的問題,因此我在這裡寫下來,以防有人用得上:))

最後,打開 應用程式級別build.gradle 檔案(位於 your_project_name/app 目錄中),並在 dependencies 節點中新增一行,以將 Flutter 模組作為來源加入到 Android 專案中:

此時,Android 應用程式應該可以編譯和建構,而且您會在 IDE 中看到 Flutter 模組。

從 Android 應用程式打開預設 Flutter 頁面

現在 Android 應用程式的安裝過程已完成,您需要準備好使用新的 Flutter 元件來啟動應用程式。幸運的是,現在安裝已完成,因此這相對容易。首先打開 AndroidManifest.xml 檔案。Flutter add-to-app 使用自訂 FlutterActivity 在 Android 中顯示 Flutter 內容,因此您需要確保在清單中宣告 FlutterActivity,方法是在 application 標籤內新增以下區塊:

接下來,打開 MainActivity.kt 檔案,並將應用程式 FloatingActionButton 顯示的 Snackbar 替換為以下程式碼,以啟動新的 FlutterActivity。

現在,當您點擊 FloatingActionButton 時,您應該會看到 Flutter 頁面直接在您的應用程式中彈出!

除了能夠啟動完整的活動螢幕(類似於您之前在 iOS 中所做的),Android 的額外好處是能夠將 Flutter 元件作為 Fragment 或自訂視圖的一部分來啟動。雖然這兩種技術超出了本教程的範圍,但您可以在官方文件找到如何使用 FlutterFragmentFlutterView

開啟其他 Flutter 螢幕

雖然能夠直接從原生 iOS 或 Android 應用程式開啟 Flutter 螢幕很棒,但考慮到使用 add-to-app 的整個想法是您可以慢慢地實作各種 Flutter 功能,因此 它實際上並沒有達到您想要的程度。要做到這一點,您很可能需要多個輸入點和多個 Flutter 元件。幸運的是,有一種方法可以在原生應用程式中建立多個 Flutter 實例,不過值得注意的是,在撰寫本文時,此功能 處於實驗階段。雖然表面層面的東西很有可能保持不變,但也可能語法或其他細節會在日後發生變化。

首先,透過打開 flutter 模組/lib 目錄中的 main.dart,更新 Flutter 模組中的程式碼以支援第二個螢幕。在 main.dart 中,透過在 main() 的宣告下方新增以下幾行,宣告您的第一個新的輸入點。請注意,此程式碼片段包含一個註釋,將此方法指定為應用程式中的新輸入點。

MySecondAppScreen 只會返回一個具有綠色主題和新標題的新 MaterialApp,以便您可以區分它和 main() 輸入點。

接下來,您可能會注意到您需要為 MySecondaryHomePage 建立另一個程式碼塊。這是一個新的 StatefulWidget,它包含 Flutter 螢幕的狀態物件。

最後,建立新的狀態物件。在本範例中,Widget 會顯示 AppBar 和 Text Widget。

您現在有兩個不同的 Flutter 螢幕可以從原生應用程式啟動。接下來,您將在現有的 Android 範例應用程式中實作新螢幕。

Android 中的多個 Flutter 輸入點

此擴展的 add-to-app 功能透過建立 FlutterEngine 類別的多個實例(與用於顯示單一預設 Flutter 螢幕的工具相同)並將它們儲存在 FlutterEngineGroup 中來執行,然後在需要時呼叫適當的引擎。首先,建立一個新的應用程式類別來初始化 FlutterEngineGroup。

接下來,建立一個輔助類別,在本例中名為 EngineBindings,它會接收輸入點的名稱,並將其懶加載到 FlutterEngineGroup 中,以便可以在原生應用程式中顯示它。這是懶加載的,因為您需要確保應用程式已完全載入,然後再開始建立 FlutterEngine,否則您可能會遇到意想不到的(且難以除錯)競爭條件。

您需要加入的最後一個類別會擴展您在上一節 Android 中使用的 FlutterActivity。建立一個名為 SingleFlutterActivity.kt 的新的 Kotlin 類別檔案,它會擴展 FlutterActivity:

在此檔案中,透過傳入新的輸入點名稱(在本例中為 “secondary”)來初始化 EngineBindings,以匹配您在 Dart 檔案中新增的輸入點的名稱,並為擷取適當的引擎撰寫一個輔助方法:

要完成 FlutterActivity,請使用新建立的引擎從 onCreate() 顯示 Flutter 螢幕。

接下來,您只需要再做幾件事就能完成此範例應用程式。回到 MainActivity,轉到原本用於啟動主要 Flutter 螢幕的 FloatingActionButton,並更改 Intent,使其啟動新的 SingleFlutterActivity 類別。

最後,打開 AndroidManifest.xml,將新的應用程式類別與 application 標籤關聯起來,並新增 SingleFlutterActivity 的活動標籤。

您現在應該能夠執行應用程式,點擊 FloatingActionButton,並看到新的螢幕,而不是預設的 Flutter Widget。

iOS 中的多個 Flutter 輸入點

您還在嗎?太好了!

接下來,您將在 iOS 範例應用程式中新增對多個輸入點的支援。回到 Xcode,打開 AppDelegate 類別,並將所有程式碼替換為這個簡化的版本,它會建立一個單一的 FlutterEngineGroup,可以在整個應用程式中訪問。

類似於您在 Android 應用程式中所做的,建立一個名為 SingleFlutterViewController.swift 的新檔案,它會擴展標準的 FlutterViewController。此類別會接收一個包含您想要使用的輸入點名稱的字串,然後建立並顯示一個新的 FlutterEngine。

最後,返回基礎 ViewController 類別,並更新 showFlutter() 函數,以便它使用指定的輸入點顯示新的 SingleFlutterViewController 類別。

更新完程式碼後,更新 Podfile 以使用 Flutter 模組的最新版本,因為您已將新的輸入點和螢幕程式碼新增到 main.dart 中。完成後,您應該能夠建構和執行 iOS 應用程式,以查看您的原生程式碼切換到新的 Flutter 元件。

總結

嘿,您做到了!恭喜!

在本教程中,您學習了如何將 Flutter 逐步新增到現有的 Android 和 iOS 應用程式中,以建立一個統一且更易於維護的程式碼庫。您已經了解了如何在兩個平台上從單一輸入點新增 Flutter,以及如何建立多個輸入點。如果您有興趣了解更多資訊,我在下方包含了一個連結,連結到 Flutter 的官方文件頁面,該頁面提供了有關 add-to-app 的更多細節資訊,以及討論 平台通道 的連結,平台通道允許您在 Flutter 和原生級別程式碼之間來回通訊。最後,請查看討論 Plugin 和如何撰寫自己的 Plugin 的連結,這些連結可以讓使用 Flutter 為多個平台開發變得更容易,無論是對您還是對開發人員社群。

我們期待看到您的跨平台應用程式實際運作!


將 Flutter 加入現有的 iOS 和 Android 程式碼庫 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】Experimenting with Dart and Wasm

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

使用 Dart 和 WebAssembly 進行實驗

作者:Liam Appelbe 和 Michael Thomsen

WebAssembly(通常縮寫為 Wasm)是一種「基於堆疊的虛擬機器的二進制指令格式」。儘管 Wasm 最初設計用於在 Web 上運行原生程式碼,但後來發展成為一種跨多個平台運行編譯程式碼的通用技術。Dart 已經是一種高度可移植和跨平台的語言,因此我們非常感興趣 Wasm 如何讓我們擴展 Dart 的這些特性。

為什麼要嘗試 Wasm?

Wasm 已經獲得了瀏覽器供應商(例如 Chrome、Edge、Firefox 和 WebKit)的廣泛支援。這使得 Wasm 成為在瀏覽器中運行二進制程式碼的一個非常有趣的方案。然而,Wasm 最初並不是為具有垃圾回收 (GC) 的程式語言(例如 Dart 和 Java/Kotlin)設計的,這使得將基於 GC 的語言有效地編譯為 Wasm 變得困難。透過參與 Wasm 專案最近的 GC 建議,我們希望既能對建議提供技術回饋,又能了解更多關於透過 Wasm 程式碼運行基於 Dart 的 Web 應用程式可能獲得的收益。

Wasm 的第二個特性是二進制 Wasm 模組與平台無關。這可能使與現有程式碼的互操作性變得更加實用:如果現有程式碼可以編譯為 Wasm,那麼所有平台上的 Dart 應用程式都可以依賴於單個共享的二進制 Wasm 模組。

在這篇文章的其餘部分,我們將討論我們使用 Wasm 和 Dart 進行的兩種形式的實驗:

  1. Dart 到 Wasm 編譯:擴展我們的 AOT 編譯器,以支援將 Dart 原始程式碼編譯為 Wasm 二進制程式碼(問題 32894)。

  2. Dart 到 Wasm 互操作:支援從 Dart 程式碼調用到編譯的 Wasm 模組(問題 3735537882)。

使用 Dart 的 Wasm 的兩種潛在用途的說明

將 Dart 編譯為 Wasm

如前所述,Wasm 最初是作為在 Web 上運行原生程式碼的一種方式。Web 傳統上由 JavaScript 程式碼提供支援,這些程式碼在虛擬機器 (VM) 中運行,該虛擬機器在 Web 應用程式運行時對 JavaScript 程式碼執行即時 (JIT) 編譯為原生程式碼。在目前以 Web 為目標的 Dart 架構中,例如 Flutter Web,Dart 應用程式程式碼會被編譯為優化的 JavaScript 以進行部署,然後當應用程式運行時,這個 JavaScript 會被 Web 平台 JIT 編譯為原生程式碼。

我們正在研究將 Dart 程式碼直接編譯為 Wasm 原生程式碼,以查看是否可以獲得在 Web 上運行原生程式碼的更直接途徑。Wasm 組合語言格式是低階的,並且比 JavaScript 更接近機器程式碼的抽象級別,這可以縮短啟動時間,並且通常可以提高效率的可預測性。

Dart 對編譯為 Wasm 的支援還處於早期研究階段,編譯器尚不完整,但我們正在進行實驗以學習。我們一直對 Wasm 作為 Dart 的編譯目標感興趣,但它的原始形式不適用於具有垃圾回收的語言。Wasm 缺乏內建的垃圾回收支援,因此像 Dart 這樣的語言必須在編譯的 Wasm 模組中包含垃圾回收實作。包含 GC 實作將非常複雜,會增加編譯的 Wasm 程式碼的大小並損害啟動時間,並且不利於與瀏覽器系統其餘部分的物件級別互操作。

幸運的是,WebAssembly 社群正在進行一項名為 Wasm GC 的工作,正在探索透過對垃圾回收語言的直接和高效能支援來擴展 Wasm 的可能性。鑑於我們長期以來對 Wasm 的興趣,我們看到了參與社群並透過編寫將 Dart 翻譯為 Wasm GC 的編譯器來提供實務經驗的機會。

現在預測這可能會帶我們走向何方還為時過早,但我們最初的原型設計顯示出非常積極的結果,初步基準測試顯示,第一幀的時間和平均幀時間/吞吐量都更快。如果您有興趣了解更多關於該專案的資訊,請查看 wasm_prototype 原始程式碼。

與 Wasm 程式碼的互操作性 (package:wasm)

除了編譯為 Wasm 之外,我們還有興趣研究 Wasm 是否可以用於以更跨平台的方式與現有程式碼整合。幾種語言支援編譯為遵循 C 調用約定的模組,並且使用 Dart FFI,您可以與這些模組進行互操作。Dart FFI 可以成為利用現有原始程式碼和函式庫的好方法,而不必在 Dart 中重新實作程式碼。

然而,由於 C 模組是平台特定的,因此使用原生 C 模組分發共享套件很複雜:它需要通用建構系統,或分發多個二進制模組(每個所需平台一個)。如果可以在所有平台上使用單個 Wasm 二進制組合語言格式,那麼分發將會更容易。然後,您不必將函式庫編譯為每個目標平台的特定於平台的二進制程式碼,而可以將其一次編譯為 Wasm 二進制模組並在任何地方運行。這可能為在 pub.dev 上輕鬆分發包含原生程式碼的套件打開了大門。

我們正在使用一個新的套件 package:wasm 來實驗 Wasm 互操作支援。這個原型是建立在 Wasmer 運行時之上的,它支援 WASI 進行作業系統互動。請注意,我們目前的原型尚不完整,僅支援桌面平台(Windows、Linux 和 macOS)。

範例:調用到 Brotli 壓縮函式庫

讓我們來看一個使用 package:wasm 來利用編譯為 Wasm 模組的 Brotli 壓縮函式庫 的範例。在範例中,我們將讀取一個輸入檔案,壓縮它,報告其壓縮率,然後解壓縮它並驗證我們是否得到了輸入。請參閱 GitHub 儲存庫以獲取完整的 範例原始程式碼。由於 package:wasm 建立在 dart:ffi 之上,如果您有 FFI 的經驗,您可能會發現這些步驟很熟悉。

有幾種方法可以將 C 程式碼編譯為 Wasm,但在這種情況下,我們使用了 wasienv。完整的詳細資訊可在 README 中找到。

對於此範例,我們將嘗試調用這些 Brotli 函數來壓縮和解壓縮資料:

1
2
3
4
int BrotliEncoderCompress(
int quality, int lgwin, int mode, size_t input_size,
const uint8_t* input_buffer, size_t* output_size,
uint8_t* output_buffer);
1
2
3
int BrotliDecoderDecompress(
size_t encoded_size, const uint8_t* encoded_buffer,
size_t* output_size, uint8_t* output_buffer);

qualitylgwinmode 參數是編碼器的調整參數。這些細節與範例無關,因此我們將僅使用這些參數的預設值。另一件需要注意的事情是 output_size 是一個輸入輸出參數。當我們調用這些函數時,output_size 必須使用我們分配的 output_buffer 的大小進行初始化,之後它將被設定為實際使用的緩衝區的數量。

第一步是使用我們編譯的 Wasm 二進制檔案來構造一個 WasmModule 物件。二進制資料應該是一個 Uint8List,我們可以使用 file.readAsBytesSync() 從檔案中讀取它來獲取。

1
2
3
var brotliPath = Platform.script.resolve('libbrotli.wasm');
var moduleData = File(brotliPath.path).readAsBytesSync();
var module = WasmModule(moduleData);

一個非常有用的除錯工具,用於確保我們的 Wasm 模組具有我們期望的 API,是 module.describe()。這將返回一個列出所有模組的導入和導出的字串。

1
print(module.describe());

對於我們的 Brotli 函式庫,這是輸出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import function: int32 wasi_unstable::fd_close(int32)
import function: int32 wasi_unstable::fd_write(int32, int32, int32, int32)
import function: int32 wasi_unstable::fd_fdstat_get(int32, int32)
import function: int32 wasi_unstable::fd_seek(int32, int64, int32, int32)
import function: void wasi_unstable::proc_exit(int32)

export memory: memory
export function: int32 BrotliDecoderSetParameter(int32, int32, int32)
export function: int32 BrotliDecoderCreateInstance(int32, int32, int32)
export function: void BrotliDecoderDestroyInstance(int32)
export function: int32 BrotliDecoderDecompress(int32, int32, int32, int32)

export function: int32 BrotliEncoderSetParameter(int32, int32, int32)
export function: int32 BrotliEncoderCreateInstance(int32, int32, int32)
export function: void BrotliEncoderDestroyInstance(int32)
export function: int32 BrotliEncoderMaxCompressedSize(int32)
export function: int32 BrotliEncoderCompress(int32, int32, int32, int32, int32, int32, int32)

我們可以看到該模組導入了一些 WASI 函數,並導出其記憶體和一堆 Brotli 函數。我們感興趣的兩個函數已導出,但它們的簽章看起來有點不同。這是因為 Wasm 只支援 32 位和 64 位整數和浮點數。指標已成為導出記憶體中的 int32 索引。

下一步是實例化模組。在實例化期間,我們必須填寫模組期望的每個導入。實例化使用建構器模式 (module.instantiate().initialization… .build())。我們的函式庫只導入 WASI 函數,因此我們可以只調用 enableWasi()

1
var instance = module.instantiate().enableWasi().build();

如果我們有額外的非 WASI 函數導入,我們可以使用 addFunction() 將 Dart 函數導入到 Wasm 函式庫中。

現在我們有了一個 WasmInstance,我們可以查詢它的任何導出函數,或檢查它的記憶體:

1
2
3
var memory = instance.memory;
var compress = instance.lookupFunction("BrotliEncoderCompress");
var decompress = instance.lookupFunction("BrotliDecoderDecompress");

接下來我們要做的是在我們的輸入檔案上使用 compressdecompress 函數。但是我們不能直接將資料傳遞給這些函數。C 函數採用指向資料的 uint8_t 指標,但在 Wasm 程式碼中,這些指標成為實例記憶體中的 int32 索引。Brotli 還使用 size_t 指標報告壓縮和解壓縮資料的大小,這些指標也變成了 int32

因此,要將我們的資料傳遞給函數,我們必須將其複製到實例的記憶體中,並將其索引傳遞給函數。我們需要 5 個記憶體區域:輸入資料、壓縮資料、壓縮大小、解壓縮資料和解壓縮大小。為了簡單起見,我們只獲取一些未使用的記憶體區域,但您也可以在函式庫中導出 malloc()free()

為了確保我們將資料放入未使用的記憶體中,我們將增加實例記憶體並將新區域用於我們的資料:

1
2
3
4
5
6
7
8
var inputPtr = memory.lengthInBytes;
memory.grow((3 * inputData.length /
WasmMemory.kPageSizeInBytes).ceil());
var memoryView = memory.view;
var outputPtr = inputPtr + inputData.length;
var outSizePtr = outputPtr + inputData.length;
var decodedPtr = outSizePtr + 4;
var decSizePtr = decodedPtr + inputData.length;

我們的記憶體區域如下所示:

1
[初始實例記憶體][輸入][輸出][輸出大小][解碼][解碼大小]

接下來,我們將輸入資料載入到記憶體中,並調用我們的壓縮函數:

1
2
3
4
5
memoryView.setRange(
inputPtr, inputPtr + inputData.length, inputData);

var status = compress(kDefaultQuality, kDefaultWindow, kDefaultMode,
inputData.length, inputPtr, outSizePtr, outputPtr);

範例的其餘部分也類似。結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
載入 lipsum.txt
輸入大小:3210 位元組

壓縮中…
壓縮狀態:1
壓縮大小:1198 位元組
空間節省:62.68%

解壓縮中…
解壓縮狀態:1
解壓縮大小:3210 位元組

驗證解壓縮…
解壓縮成功 :)

試用 package:wasm

如果您有興趣嘗試 Wasm 互操作,請查看 package:wasm 的 README 以獲取說明。

路線圖

Wasm 編譯和 Wasm 互操作都是實驗。如果這些實驗證明 fruitful,我們計劃繼續開發它們,並最終將它們產品化為穩定、受支援的版本。但是,如果我們了解到某些東西沒有按預期工作,或者看到缺乏興趣,我們將停止實驗。

我們正在進行這些實驗以學習,其中包含兩個主要組成部分。首先,我們想了解技術上支援 Wasm 的可行性,以及這種支援的特性可能是什麼。它可以使 Dart 程式碼更快、更小或更可預測嗎?其次,我們有興趣探索 Wasm 可能解鎖哪些新的技術功能,以及這些功能可能為 Dart 開發人員帶來哪些新的使用案例。我們可以使與原生程式碼的互操作更具可移植性嗎?

您認為 Wasm 如何應用於您的需求?您認為您將用它做什麼?我們很樂意聽到您的想法。請在 Dart misc 討論群組 上告訴我們。

【文章翻譯】Google I/O spotlight: Flutter in action at ByteDance

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

Google I/O 聚焦:字节跳动 Flutter 实战

Note: This article was originally written in Chinese by the ByteDance team and translated into English.

Flutter, a technology that ByteDance has been utilizing and contributing to for several years now, was recently highlighted on the main stage of Google I/O. Developed and open-sourced by Google, the multi-platform framework for front-end UI development has garnered over 120,000 stars on GitHub.

At Google I/O, Zoey Fan, one of Flutter's product managers, talked about how the framework was adopted at ByteDance.
More than 70 apps by ByteDance use Flutter as their multi-platform solution.
Flutter has reduced development time by 33%, as compared to developing separate apps for Android and iOS.

Today, there are over 500 Flutter developers at ByteDance, with over 200 actively developing with the framework. These developers utilize Flutter not only for mobile apps but also experiment with it on web, desktop, and embedded platforms.

Beyond this, ByteDance has conducted fundamental work throughout the organization and made significant contributions to the Flutter project by submitting dozens of pull requests (PRs).

How did ByteDance make Flutter truly work for them?

The story of Flutter at ByteDance began two years ago.

At the time, the ByteDance front-end engineering team noticed that many teams within the company needed to develop for multiple platforms, but lacked a tool to achieve high-efficiency, high-performance, multi-platform development.

When Google open-sourced Flutter, the ByteDance team discovered that with Flutter, they only needed to develop the app once to support platforms such as Android, iOS, and web. Also, because Flutter has its own rendering engine, they could achieve more consistent performance across platforms.

With Flutter, the Android, iOS, and web versions of an app automatically stay in sync. There is no need to design and program the UI separately for each platform, eliminating a significant portion of redundant work.

To support business development more efficiently, the ByteDance team performed fundamental work on the framework itself, such as optimizing performance, creating app frameworks, containerizing, and supporting “add to app.” They also improved Flutter performance tools, including improvements to the Frames Per Second (FPS) info in the Frame chart and the timeline events chart. Both of these charts are part of the Performance View in Flutter DevTools.

When adopting Flutter, the ByteDance team encountered some unique challenges. For example, Flutter must be added to the app installation package, increasing the size of the app downloaded by users. Additionally, Flutter uses the Dart programming language, which is larger in size than native code, further increasing the package size.

The ByteDance team started a special plan to optimize the package size by compressing the iOS data section and stripping out the Skia library and other libraries (such as BoringSSL, ICU, text rendering, and libwebp). They analyzed Flutter Dart code against iOS native code and found that to implement the same business feature, the Dart code generated more machine code instructions. To close the gap, they reduced alignment of instructions, removed debugging trap instructions, dropped redundant initialization of stores with null, removed RawInstruction headers with bare instructions mode, compressed StackMaps, removed CodeSourceMap, and so on.

Individually, each of these optimizations reduced the package size by 0.2 to 4 MB and significantly reduced the total package size when combined. The ByteDance team shared their experience with Google engineers, and many improvements made their way to the Flutter open source project for the benefit of the larger community.

However, when ByteDance released their first Flutter app, new issues emerged. Users asked: ‘Why is the UI so janky when I scroll in the app?’

When the ByteDance team looked into the issue, they saw that when a FlutterView extended a TextureView, the UI was noticeably jankier than when it extended SurfaceView. However, in the official Timeline tool, the UI thread time and GPU thread time for each rendered frame are about the same, with TextureView pulling a bit ahead occasionally.

The metrics contradicted the real-world user experience, which puzzled the team.

At first, the team used the Timeline tool to troubleshoot the issue, to no avail. After digging into the tool’s source code, they discovered the root cause of the issue.

SurfaceView had better performance than TextureView. Because SurfaceView had its own surface, and rendering was performed in its own OpenGL context, it could interact with SurfaceFlinger independently and took full advantage of triple-buffering. On the other hand, TextureView was a regular view that depended on the surface of its host window for rendering. That meant the rendering wasn’t performed immediately after the UI and GPU threads had finished their work but needed to wait for the native main thread and renderThread before the view could interact with SurfaceFlinger. That was a much longer rendering pipeline than that of SurfaceView.

These findings not only helped the team eliminate the jank but led to 10 PRs being submitted to the Flutter open source project. With this fundamental work done, Flutter eventually became the go-to framework for multi-platform app development at ByteDance. Soon, the ByteDance team’s work with Flutter will be available to external developers using their mobile development framework, veMARS, benefiting the entire developer community.

From experiment to production: How ByteDance put Flutter into use

It wasn’t exactly smooth sailing for ByteDance to put Flutter into real-world use.

At first, the ByteDance team chose a mature product and planned to re-implement the app’s video playback feature with Flutter.

The feature, originally written in native code for Android and iOS, wasn’t straightforward to rewrite with Flutter. After six months, the team came to the conclusion that it would be difficult to make all the live data compatible and challenging to update the existing business logic.

The team decided that it wasn’t productive to update the existing features of a mature product with the new framework. Flutter’s strengths would be better used in a brand-new app. The team lead said, “In a mature product, everything is already well built with native Android or iOS technology. There isn’t much gain in re-implementing the features with Flutter only to make minor improvements. It also increases the package size since the Flutter engine is included in the package. In new products or new scenarios, however, Flutter can greatly increase our productivity.”

With this changed mindset, the team turned their focus to new business areas such as education.

One of their education apps in China helps students learn the order of strokes of Chinese characters; the team wanted to add a stroke tracking feature.

To implement it, the team took inspiration from some open-source projects and decided to use SVG paths to represent strokes. The paths would then be adjusted and positioned to compose the characters:

They defined the skeleton of each stroke to guide the movement of the virtual brush pen, so the pen moves just like it would in calligraphy:

Based on the defined order of the skeletons, a circle with a certain radius is drawn along each skeleton, and together these circles form the stroke. After that, the team added keyframes to ensure that the frame rate of the animation is high enough to avoid jank.

That is how they created the smooth tracking effect, as shown in the following GIF:

The feature, built with Flutter, now supports over 9,000 Chinese characters, including most of the commonly used characters. Compared to developing with native code, Flutter saved time and resources.

Today, many apps by ByteDance employ a hybrid approach to development, combining the strengths of Flutter and other technologies, with newer apps leaning towards pure Flutter. For apps such as Xigua Video, TikTok Volcano, and Open Language, Flutter increased the productivity of the teams by about 33%.

ByteDancers Embrace the Latest Technology

Even now, the Flutter team at ByteDance continues to explore the latest technologies. According to the team lead, “We have in our team many tech enthusiasts with global vision, and will continue to explore global technology developments and discuss the implementation of technology. We have close connections and collaborations with many tech companies. We have quarterly sync meetings with Google, for instance, to exchange progress, thoughts, needs, and ideas from both sides.”

One day, the maintainer of the Dart open source project on GitHub came to the ByteDance team lead with the following remarks, “Someone from your team submitted more than a dozen PRs to Dart and they’re all very good and well thought out.”

The Dart open source project maintainer was talking about Frank. Frank is a passionate open-source contributor and just got his bachelor’s degree three years ago. His journey in the open source world first started during his first year of university in 2015. One of the projects he created and open-sourced on GitHub had over 700 stars. “It has had hundreds of downloads per year, and many game developers use it to create demos”, Frank said.

After graduation, Frank joined the Flutter team at ByteDance and became one of the most active open-source contributors on the team, contributing a number of PRs to Dart and Flutter. Frank remembers that when he was working on the package size issues, he proactively followed up on a relevant issue on the Dart GitHub project and noticed that the Specializer component could use some further tuning. He created a patch with his improvements to the Dart compiler middleware and submitted it to the project. The patch wasn’t accepted initially because of the large number of code blocks affected and a few minor concerns. He modified the patch seven times before it was accepted, and it was merged into the code base a week later.

There are many other open-source enthusiasts like Frank in the Flutter team at ByteDance.

The ByteDance team summarized this passionate attitude toward innovation with the following words:

“There are indeed many people in the industry who prefer mature technology, but it takes time for every technology to mature, and there will always be people like us who love to stay on the cutting edge.”

This is especially true for something as novel as Flutter. There needs to be some daring people who take the first steps. At ByteDance, the Flutter engineering team, as well as the engineering teams that they support, actively try and embrace new technologies. Doing this benefited ByteDance tremendously and greatly increased our productivity.

ByteDance has always wanted to be part of things that could push the industry forward, and Flutter is likely to be one of those things.


Google I/O spotlight: Flutter in action at ByteDance was originally published in Flutter on Medium, where people are continuing the conversation by highlighting and responding to this story.

【文章翻譯】How Dart’s null safety helped me augment my projects

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

我將一個正在執行的應用程式和一個已發佈的套件遷移到空安全,體驗太棒了!

關於作者: Waleed Arshad 是一位核心行動技術專家,也是一位充滿熱情的跨平台開發者,更是巴基斯坦第一位獲得 Flutter Google 開發專家認證的人。從卡拉奇 FAST 畢業後,他在業界工作了五年多,目前在 Tendermint 的 Flutter 開發者體驗團隊工作。他同時也領導著巴基斯坦的 Flutter 社群。

隨著 Flutter 2 的推出,空安全功能已在 Flutter 的穩定頻道上可用。這篇文章將討論我將應用程式和套件遷移到空安全以及從頭開始建立空安全應用程式的個人經驗。簡而言之,結果令人驚豔!

如果您不熟悉 Flutter 的空安全功能,請查看空安全公告。如果您想完全理解空安全,請查看Dart 空安全文件

本文描述了我使用空安全的兩種經驗:

  • 遷移應用程式和套件
  • 在空安全環境中編寫新程式碼

遷移應用程式和套件

當我第一次將 Flutter 升級到 2.0 版(支援空安全的版本)並將 Dart SDK 版本更新到 2.12 時,我在 Flutter 應用程式的 pubspec.yaml 檔案中看到了很多錯誤。我最初想嘗試手動遷移到空安全(即不使用遷移工具進行遷移),所以我開始手動解決空安全錯誤——在我的程式碼中到處添加問號和驚嘆號。我故意這樣做,只是為了理解 Flutter 團隊在 空安全遷移工具 上所做的所有努力,以自動化更改和更新程式碼的流程!經過一些實驗後,我還原了所有手動進行的更改,並使用了該工具的魔力來完成應用程式的遷移。

該應用程式是一個實驗性的 COVID-19 統計應用程式,其程式碼是完全開源的。您可以在 GitHub 上找到它。

看到遷移工具在我的專案中所做的所有程式碼更改真的很酷——例如在可空值中添加問號,以及在遷移工具檢測到值永遠不會為空時添加驚嘆號。

以下是一個遷移工具自動添加問號和驚嘆號的例子。_homeCountry 是一個名為 HomeCountry(也是可空值)的類別的可空值屬性。因此,為了保護對 _homeCountry 其中一個屬性的存取,該工具添加了問號運算子。

遷移之後,程式碼中的一些問題變得明顯,這是最好的部分

其中一個問題是,一些可空值的字串作為列表傳遞給 shared_preferences Plugin 的內部函數。因為這些值是可空值的,所以工具將整個列表類型設為 <String?>[],這開始產生錯誤,因為該函數接受的類型是 <String>[]

這個問題的一個簡單解決方案是移除問號,並使列表類型與函數參數的類型匹配。當我這樣做時,分析器開始提示可空值類型 (String?) 無法賦值給不可空值類型 (String)。

為了解決這個問題,我將 HomeCountry 類別的每個屬性都設為不可空值,並在建構函數中添加了 required 關鍵字。這意味著現在在初始化 HomeCountry 時必須傳遞參數。我不必更改 setHomeCountry 函數,因為傳遞給列表的變數現在是不可空值的。

這個更改防止了我在程式碼中錯誤地將空值傳送給 shared preferences,這是空安全功能提供的非常有價值的輸入!

空安全發現的另一件事是一個可能導致執行時崩潰的錯誤。請參見以下程式碼片段:

A screenshot of code for setState() that assumes (but doesn’t check) that list isn’t null.

因為 list 是一個可空值的變數,讀取其基於索引的元素可能會導致崩潰。遷移到空安全後,我無法編譯應用程式,因為在讀取此列表中的值之前沒有進行空檢查。

最終,我添加了一個空檢查,以使程式碼能夠編譯,並防止應用程式在此程式碼處崩潰。令人驚訝的是,遷移是如何幫助我發現一個實際的錯誤!

In this code sample, the code for setState is protected by a null check: `if (list != null)`
setState 的程式碼現在有效,因為 list 不為空。

我也碰巧遷移了一個非常小的套件,您可以在 pub.dev 上找到它,名為 progress_indicators。我驚訝地發現,當遷移工具得出結論認為這些變數在使用前已初始化時,它添加了 late 關鍵字而不是問號。

A screenshot of source code for a class that has `late` fields.

在空安全環境中編寫新程式碼

現在 Flutter 具有空安全功能,建立新的應用程式可以提供更好的開發體驗。在空安全環境中編寫新程式碼還可以更好地理解程式碼流程,並且能夠編寫防崩潰程式碼。您現在無法使用如下的類別建立可編譯的程式碼:

1
2
3
4
5
class MyClass {
String a;

MyClass({this.a});
}

這段程式碼會導致編譯時錯誤,提示您將 a 標記為可空值,在建構函數中添加 required 關鍵字,或添加初始化器。程式碼確保 a 永不為空。因此,根據您的使用案例,您可以這樣做:

1
2
3
4
5
class MyClass {
String? a;

MyClass({this.a});
}

或者您可以這樣做:

1
2
3
4
5
class MyClass {
String a;

MyClass({required this.a});
}

請注意,從 Flutter 2(和 Dart 2.12)開始,您不再需要在 required 關鍵字前添加 @ 符號。

或者您可以將 a 保持為可選的,但添加一個初始化器,如果沒有傳遞值,則賦予它一個預設值:

1
2
3
4
5
class MyClass {
String a;

MyClass({this.a = ''});
}

此外,您現在可以建立自己建立的類別的可空值變數:

1
2
3
4
5
6
7
8
class MyClass {
String? a;

MyClass({this.a});
}

// 在主程式碼中的某處
MyClass? myClass;

因為 myClass 具有可空值類型,如果您編寫如下程式碼,編譯器將會發出錯誤:

1
print(myClass.a);

錯誤如下:

屬性 ‘a’ 無法無條件存取,因為接收器可以為 ‘null’。

您可以透過添加問號來修復該錯誤:

1
print(hello?.a);

Dart 的空安全功能確保您編寫更安全、更不易出錯的程式碼。在編譯時發現與空變數相關的錯誤是對開發體驗的寶貴補充。前面的範例顯示了編譯器如何在檢測到可能導致應用程式在執行時崩潰的空指標異常時阻止您編譯程式碼。這顯然意味著編譯器會盡可能編寫空安全的程式碼(除非您使用 ! 運算子強制展開所有內容)。

總之,Dart 的健全空安全功能是一個可靠的起點,可以用於構建更安全、更快、更可靠的應用程式!整體的編碼體驗現在更加規範和更有條理。我建議您將舊的 Dart 應用程式遷移到空安全,以了解它的工作原理。也許您會幸運地在舊程式碼中發現並修復一些錯誤!

編碼愉快!:)


Dart 的空安全功能如何幫助我增強我的專案 最初發佈在 Medium 的 Dart 上,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】Improving Platform Channel Performance in Flutter

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

過去幾年,我一直對「如何讓 Flutter 與其主機平台之間的通訊更快、更容易?」這個問題感到興趣。這對 Flutter Plugin 開發人員和增加到應用程式開發人員來說,是一個特別感興趣的問題。

Flutter 與主機平台之間的通訊通常使用 [平台通道](https://flutter.dev/docs/development/platform-integration/platform-channels) 完成,因此我的精力一直集中在這裡。在 2019 年後期,為了解決使用平台通道所需的過多樣板和 [字串類型](https://wiki.c2.com/?StringlyTyped) 程式碼,我設計了一個程式碼產生套件 [Pigeon](https://pub.dev/packages/pigeon),它使平台通道類型安全,並且團隊持續改進它。在 2020 年春季,我對 [平台通道和外部函式介面 (FFI) 效能進行了審查](https://docs.google.com/document/d/1bD_tiN987fWEPtw7tjXHzqZVg_g9H95IS32Cm609VZ8/edit)。現在,我將目光放在 [改進平台通道的效能](https://docs.google.com/document/d/1oNLxJr_ZqjENVhF94-PqxsGPx0qGXx-pRJxXL6LSagc/edit?usp=sharing) 上。由於 Pigeon 建立在平台通道之上,而我計畫在 Pigeon 之上建立一個 [多個 Flutter 實例的資料同步解決方案](http://flutter.dev/go/data-sync),這是一個很好的機會,可以幫助滿足開發人員的許多不同需求,以及我的計劃。

經過一番調查,我能夠識別出透過平台通道傳送的資料的冗餘副本,並且能夠將其移除。您將在下面找到該變更的結果以及識別和移除這些副本的相關工作概述。

結果

在移除透過平台通道從 Flutter 傳送到主機平台的 1 MB 二元資料時,並響應 1 MB 的資料,我們看到 [iOS 上的效能大約提高了 42%](https://flutter-flutter-perf.skia.org/e/?begin=1620764044&end=1621044607&queries=sub_result%3Dplatform_channel_basic_binary_2host_1MB%26test%3Dmac_ios_platform_channels_benchmarks_ios&requestType=0)。在 Android 上,結果稍微複雜一些。我們的自動化效能測試 [大約提高了 15%](https://flutter-flutter-perf.skia.org/e/?begin=1621972627&end=1622677144&queries=sub_result%3Dplatform_channel_basic_binary_2host_1MB%26test%3Dlinux_platform_channels_benchmarks&requestType=0),而本地測試在遷移到新的 [BinaryCodec.INSTANCE_DIRECT](https://github.com/flutter/engine/blob/b3ebb6dd62cefe3c30a7bd15ed73c578030140e2/shell/platform/android/io/flutter/plugin/common/BinaryCodec.java#L27) 編碼器時,[大約提高了 52%](https://github.com/flutter/engine/pull/26331#issuecomment-854071096)。這種差異可能是因為自動化效能測試在舊設備上運行,但也可能是微基準測試在舊設備上的執行方式產生的假象(例如,不斷地讓垃圾收集器執行)。您可以在 [platform_channels_benchmarks/lib/main.dart](https://github.com/flutter/flutter/blob/00bfe9061369bb6fdfe4a74fb27086b77df107bf/dev/benchmarks/platform_channels_benchmarks/lib/main.dart#L165) 中找到自動化效能測試的原始碼。

對於使用 StandardMessageCodec 的平台通道,我發現效能增益較小([使用 14k 負載大約為 5%](https://flutter-flutter-perf.skia.org/e/?begin=1620764044&end=1621044607&queries=sub_result%3Dplatform_channel_basic_standard_2host_large%26test%3Dmac_ios_platform_channels_benchmarks_ios&requestType=0))。我使用一個大型支援類型陣列對其進行測試,以對編碼和解碼進行壓力測試。我發現,MessageCodecs 的編碼和解碼時間遠遠超過在平台之間複製訊息所花費的時間。大部分的編碼時間都是由於對資料結構進行遞迴,並使用反射來找出其內容的成本所致。

因此,根據您使用平台通道的方式和設備,您的結果可能會有很大差異。如果您想要使用平台通道進行最快的通訊,那麼您應該使用 BasicMessageChannels,並在 iOS 上使用 [FlutterBinaryCodec](https://github.com/flutter/engine/blob/b3ebb6dd62cefe3c30a7bd15ed73c578030140e2/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h#L52),在 Android 上使用 [BinaryCodec.INSTANCE_DIRECT](https://github.com/flutter/engine/blob/b3ebb6dd62cefe3c30a7bd15ed73c578030140e2/shell/platform/android/io/flutter/plugin/common/BinaryCodec.java#L27),並為編碼和解碼訊息開發自己的協定,該協定不依賴於反射。(實作新的 MessageCodec 可能更乾淨。)

如果您想試用新的、更快的平台通道,它們現在已在 [master 通道](https://flutter.dev/docs/development/tools/sdk/upgrading#switching-flutter-channels) 上提供。

詳細複製移除

如果您對如何實現這些結果以及我必須克服的問題不感興趣,那麼現在就可以停止閱讀了。如果您喜歡了解詳細資訊,請继续阅读。

平台通道 API 自 2017 年以來沒有太大變化。由於平台通道是引擎和 Plugin 操作的基礎,因此它們不容易更改。雖然我對平台通道的運作方式有一定的了解,但它們在一定程度上是複雜的。因此,改進其效能的第一步是準確地了解它們的運作方式。

下圖概述了框架在使用平台通道與 iOS 進行通訊時從 Flutter 遵循的原始流程:

圖表中的一些收穫:

  • 訊息會從 UI 線程跳轉到平台線程,然後再跳回 UI 線程。(在 Flutter 引擎術語中,UI 線程是執行 Dart 的位置,而平台線程是主機平台的主線程。)
  • 訊息及其響應使用 C++ 作為介於 Flutter 與主機平台目標語言之間的介面層。
  • 訊息的資訊在到達 Objective-C (Obj-C) 處理程式之前被複製了 4 次(步驟 3、5、7、8)。步驟 3 和 8 執行翻譯,而步驟 5 和 8 執行複製,將資料的所有權轉移到新的記憶體佈局。相同的過程反向重複以進行回覆。
  • 步驟 1、9 和 16 是使用 Flutter 的開發人員編寫的程式碼。

從 Flutter 傳送訊息到 Java/Kotlin 類似,只是在 C++ 和 Java 虛擬機器 (JVM) 之間有一個 Java 本機介面 (JNI) 層。

在確定平台通道的運作方式後,很明顯,消除在這些層之間傳輸資料時進行的複製(例如,從 C++ 到 Obj-C)是改進效能的顯而易見的方法。為了實現這一點,Flutter 引擎必須將資料放置在記憶體中,以使其可以直接從 Java/Obj-C 存取,並且具有與主機平台相容的記憶體管理語義。

平台通道訊息最終由主機平台的 MessageCodec 的 decodeMessage 方法使用。在 Android 上,這意味著一個 [ByteBuffer](https://github.com/flutter/engine/blob/58459a5e342f84c755919f2ad5029b22bcddd548/shell/platform/android/io/flutter/plugin/common/MessageCodec.java#L38),在 iOS 上,這意味著一個 [NSData](https://github.com/flutter/engine/blob/58459a5e342f84c755919f2ad5029b22bcddd548/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h#L38)。C++ 中的資料必須符合這些介面。在處理此問題時,我發現訊息的資訊儲存在 C++ 記憶體中,作為一個 [std::vector](https://github.com/flutter/engine/blob/70ebfc3610c38c463469ffedea85578f35ccc0a0/lib/ui/window/platform_message.h#L39),位於由 [共用指標](https://en.wikipedia.org/wiki/Smart_pointer) 維護的 PlatformMessage 物件中。這意味著開發人員在將資料從 C++ 傳送到主機平台時,無法安全地移除複製,因為他們沒有保證資料在傳送到主機平台後不會被 C++ 變異。此外,我必須小心,因為 BinaryCodec 實作將 encodeMessage 和 decodeMessage 視為無操作,這可能導致使用 BinaryCodec 的程式碼在不知情的情況下收到直接 ByteBuffer。雖然有人可能會對 MessageCodec 的變更感到意外,但很少有人實作自己的編碼器。另一方面,使用 BinaryCodecs 非常普遍。

在閱讀程式碼後,我發現,雖然 PlatformMessage 由共用指標管理,但它在語義上是唯一的指標。目的是一次只允許一個客戶端存取它(這並不完全是這樣,因為在線程之間傳遞 PlatformMessage 時,暫時會存在多個副本,但这僅僅是为了方便,而并非真正意图)。這意味著我們可以從共用指標遷移到唯一指標,允許我們安全地將資料傳遞到主機平台。

在 [遷移到唯一指標](https://github.com/flutter/engine/commit/7424400f07be684bd87633bbe2d263821181345a#diff-d5a1c9b29bed0d80dc68f228550643925a216e65173364e1ae5a03067b60160d) 後,我必須找到一種方法,將資訊的所有權從 C++ 傳遞到 Obj-C。(我首先實作了 Obj-C,稍後將更詳細地討論 Java)。資訊儲存在一個 std::vector 中,它沒有辦法釋放底層緩衝區的所有權。您唯一的选择是复制出数据、提供一个包含std::vector的适配器、或消除std::vector的使用。

我的第一次嘗試是子類化 NSData,它會 std::move std::vector 並從那裡讀取其資料,從而消除複製。這種嘗試效果不佳,因為結果證明 NSData 是 [Foundation](https://developer.apple.com/documentation/foundation?language=objc) 中的 [類別叢集](https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/ClassClusters/ClassClusters.html)。這意味著您不能只子類化 NSData。在閱讀了許多 Apple 的文件之後,他們似乎建議使用組合和訊息轉發,使物件的行為和外觀像 NSData 一樣。這會欺騙使用代理物件的人,除了那些呼叫 -[NSObject isKindOfClass:] 的人以外。雖然這不太可能,但我無法排除這種可能性。雖然我認為可能有一些與 Obj-C 執行時相關的調整,可以使物件按照我想要的方式運行,但它變得越來越複雜。我選擇將記憶體從 std::vector 移動到 [我們自己的緩衝區類別](https://github.com/flutter/engine/commit/b0bb8eab1d2f7e58230298c28a28ddfeddedeb64#diff-d5a1c9b29bed0d80dc68f228550643925a216e65173364e1ae5a03067b60160d) 中,該類別允許釋放資料的所有權。這樣,我就可以使用 -[NSData dataWithBytesNoCopy:length:] 將資料的所有權傳遞到 Obj-C。

在 Android 上複製這個過程證明更困難。在 Android 上,平台通道符合 ByteBuffer,它具有 [直接](https://docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html) [ByteBuffers](https://docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html) 的概念,允許 Java 程式碼直接與以 C/C++ 方式佈局的記憶體進行介面。我在短時間內實作了遷移到直接 ByteBuffers,但我沒有看到預期的改進。我花費了很長時間學習 Android 分析工具,最終選擇了追蹤語句,因為那些工具失敗了,或者返回了我無法相信的結果。事實證明,從平台線程到 UI 線程排程對平台通道訊息的回覆非常慢,而且似乎慢到這種程度,這種減速程度會隨著訊息的負載而增加。長話短說,我在編譯 Dart VM 時使用了錯誤的編譯標誌,以為 -no-optimization 代表 -no-link-time optimization,但該標誌實際上是針對運行時優化的。

在我發現自己的錯誤時,我忘記了在將資料傳送到 Flutter 客戶端程式碼(特別是透過自訂 MessageCodecs 或 BinaryCodec 的客戶端)時使用直接 ByteBuffer 的後果。傳送直接 ByteBuffer 意指您有一個 Java 物件正在與 C/C++ 記憶體進行通訊,因此,如果您刪除 C/C++ 記憶體,那麼 Java 會與隨機垃圾進行交互,並且可能會因作業系統的存取衝突而當機。

效仿 iOS 的做法,我嘗試將 C/C++ 記憶體的所有權傳遞給 Java,以便在 Java 物件被垃圾收集時,它會刪除 C/C++ 記憶體。結果證明,當直接 ByteBuffer 是透過 JNI 透過 [NewDirectByteBuffer](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#NewDirectByteBuffer) 建立時,這是做不到的。JNI 沒有提供在 Java 物件刪除時得知的掛鉤。您無法子類化 ByteBuffer,以便在它被終止時呼叫 JNI。唯一的希望是在上圖中的步驟 5 中從 Java API 分配直接 ByteBuffer。透過 Java 分配的直接 ByteBuffers 沒有這個限制。但是,在 Java 中引入新的入口點將是一個巨大的變革,而且任何使用過 JNI 的人都知道這是很危險的。

相反,我選擇請團隊接受在 decodeMessage 呼叫中使用直接 ByteBuffers。起初,我在 MessageCodec 中引入了一個新的方法,bool wantsDirectByteBufferForDecoding(),以確保沒有人獲得直接 ByteBuffer,除非他們請求它,並且知道其語義(也就是,當底層 C/C++ 記憶體仍然有效時)。這被證明是複雜的,並且令人擔憂的是,開發人員可能會訂閱,但不知道直接 ByteBuffers 的語義,因為它們的運作方式與典型的 ByteBuffers 相反,可能會在他們身下刪除其 C 記憶體支援。儲存編碼的緩衝區是不尋常的使用方式,而且不太可能使用,但團隊無法排除這種可能性。經過多次討論和協商,我們決定每個 MessageCodec 都會獲得一個直接 ByteBuffer,在呼叫 decodeMessage 後會被清除。這樣,如果有人快取編碼的訊息,那麼如果他們在底層 C 記憶體被清理後嘗試使用 ByteBuffer,他們就會在 Java 中得到一個確定性的、適當的錯誤。

讓每個人都能獲得直接 ByteBuffers 效能提升的優點效果很好,但這對 BinaryCodec 來說是一個重大變革,其 encodeMessage 和 decodeMessage 實作是無操作的,它們只是將輸入作為返回值轉發。為了保持 BinaryCodec 的相同記憶體語義,我引入了一個 [新的實例變數](https://github.com/flutter/engine/blob/01d1ed459a313f19e2e01cf8d62331d19b907637/shell/platform/android/io/flutter/plugin/common/BinaryCodec.java#L29),它控制解碼的訊息是直接 ByteBuffer(新的、更快的程式碼)還是標準 ByteBuffer(舊的、更慢的程式碼)。我們無法建立一種方法,讓 BinaryCodec 的所有客戶端都能獲得效能提升。

未來工作

現在已經消除了複製,我下一步改進 Flutter 與主機平台之間通訊的努力是:

  1. 為 Pigeon 實作自訂 MessageCodec,該編碼器不依賴於反射,以實現更快的編碼和解碼。
  2. 實作 FFI 平台通道,讓您可以在不跳轉 UI 和平台線程之間的情況下,從 Dart 呼叫主機平台。

我希望您喜歡這次對此效能改進的詳細資訊的深入探討!


改進 Flutter 中的平台通道效能 最初發表在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】Implementing structs by value in Dart FFI

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

在 Dart FFI 中實作傳值結構體

Dart 2.12 版本中,我們擴充了 C 語言互通功能 Dart FFI,使其能夠 傳值結構體。本文將討論將此功能加入 Dart SDK 的過程。如果您對低階語言實作細節或平台傳值結構體的慣例感興趣,請继续阅读。

本文將討論開發 API 和找出傳值結構體功能的 ABI(應用程式二進位制介面)。在我们開發此功能(以及其他 Dart FFI 功能)的兩年中,我們發現了許多需要變更 API 的限制。ABI 的過程同樣有趣,它說明了您可以採取多種方法來確定一個難題的細節。

C/C++ 中的傳值和傳址

如果您不是每天都撰寫 C 語言程式碼,這裡快速回顧一下。假設我們在 C 語言中有以下結構體和函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Coord {
double x;
double y;
struct Coord* next;
};

struct Coord TranslateByValue(struct Coord c) {
c.x += 10.0;
return c;
}

void TranslateByPointer(struct Coord* c) {
c->x += 10.0;
}

然後,我們可以在一些簡單的 C 語言程式碼中使用這些函數。假設我們有一個局部變數 c1

1
Coord c1 = {10.0, 10.0, nullptr};

如果我們將 c1 傳遞給 TranslateByValue,則參數是傳值的,這使得被調用者實際上操作結構體的副本:

1
Coord c2 = TranslateByValue(c1);

這表示 c1 保持不變。

但是,如果我們使用指向包含 c1 的記憶體的指標傳址 c1,則 c1 將被就地修改:

1
TranslateByPointer(&c1);

c1.x 現在包含 20.0

API 設計之旅

最初的 Dart FFI 原型 已經支援傳遞結構體的指標。但是,我們多次重新設計了 API,以適應各種使用案例和限制。

初始設計

我們的初始設計允許在記憶體中配置結構體,將這些指標傳遞給 C 語言,並修改結構體的欄位。透過這種方法,Struct 類別擴充了 Pointer 類別:

1
2
3
4
5
6
7
8
9
10
11
class Coordinate extends Pointer {
@Double()
external double x;

@Double()
external double y;

external Pointer next;

static int get sizeOf => sizeOf&lt;Coordinate&gt;();
}

Dart FFI 使用者撰寫上述程式碼片段,Dart FFI 內部會產生 sizeOf 的實作以及 xynext 的 getter 和 setter 實作。

但是,兩年前我們意識到 這個設計存在一個問題。透過讓 Coordinate 擴充 Pointer,我們無法區分 CoordinateCoordinate*

區分 Coordinate 和 Coordinate*

我們 引入Struct 到 Dart FFI,並讓結構體擴充此類別:

1
2
3
4
5
6
7
8
9
class Coordinate extends Struct {
@Double()
external double x;

@Double()
external double y;

external Pointer&lt;Coordinate&gt; next;
}

現在,Dart 中的 Pointer<Coordinate> 代表 C 語言中的 Coordinate*,Dart 中的 Coordinate 代表 C 語言中的 Coordinate

這表示 next 欄位的類型為 Pointer<Coordinate>,這使得 @Pointer 註釋變得冗餘。因此,我們 擺脫了 Pointer 註釋

1
2
3
4
5
6
7
8
9
class Coordinate extends Struct {
@Double()
external double x;

@Double()
external double y;

external Pointer&lt;Coordinate&gt; next;
}

因為我們現在將結構體的指標表示為 Pointer 物件,所以我們開始在 Pointer 上使用 allocate 工廠:

1
final c = Pointer&lt;Coordinate&gt;.allocate();

為了存取 Pointer<Coordinate> 的欄位,我们需要一个 Coordinate 類型的物件,因為該物件具有欄位 xynext。為此,我們已經在 Pointer 上使用了 load 方法。

1
c.load&lt;Coordinate&gt;().x = 10.0;

當然,在調用 load 時必須寫 <Coordinate> 很冗長。(必須撰寫類型引數與從 Pointer<Uint8> 中載入 Dart int 相同。)我们需要在 load 上使用此類型引數的原因是向 Dart 類型系統指定此方法的返回類型。

擴充方法來救援

Dart 2.7 引入了 擴充方法。透過擴充方法,我們可以在 Pointer<T> 中的類型引數 T 上進行 模式匹配

1
2
3
extension CoordinatePointer on Pointer&lt;Coordinate&gt; {
Coordinate get ref => load();
}

在類型引數上進行模式匹配使我們能夠 擺脫調用站點的冗長

1
c.ref.y = 10.0; // ref 被模式匹配為 Coordinate 類型。

我們還可以利用擴充方法模式匹配,使 Struct<S> 的類型引數變得冗餘,將使用者結構體的定義變更為:

1
2
3
4
5
6
7
8
9
class Coordinate extends Struct {
@Double()
external double x;

@Double()
external double y;

external Pointer&lt;Coordinate&gt; next;
}

之前,類型引數 <S> 限制了 Struct 欄位 Pointer<S> addressOf。相反,我們將欄位變更為擴充 getter:

1
2
3
extension&lt;T extends Struct&gt; on T {
Pointer&lt;T&gt; get addressOf =&gt; ...
}

停止洩漏後端儲存

從 C 語言向 Dart 返回傳值結構體時,我們不希望使用 malloc 分配 C 語言記憶體來儲存結構體,因為這樣速度會很慢,並且 會讓使用者承擔釋放記憶體的負擔。因此,我們將結構體複製到 TypedData 中,Coordinate 可以使用 PointerTypedData 作為後端儲存。

但是,在第一次重新設計中引入的 addressOf 的類型為 Pointer。此類型表示它始終由 C 語言記憶體支援,但現在已不再如此。

因此,我們 棄用了 addressOf

為了優化

最後一步是要求調用各種 Dart FFI 方法,包括與結構體相關的方法,都具有 編譯時常數類型引數

1
2
3
extension on Pointer&lt;T extends NativeType&gt; {
T load&lt;T extends NativeType&gt;() =&gt; ...
}

調用方法允許我們更好地優化程式碼,並且更符合 C 語言的語義。

請注意,最後一個變更會在 Dart 2.12 中觸發棄用通知,並且此變更在 Dart 2.13 中強制執行。

ABI 探索之旅

現在 API 已經到位,下一個問題是:在傳值或返回結構體時,C 語言期望這些結構體在哪裡? 這就是所謂的應用程式二進位制介面 (ABI)。

文件

很自然地會去查閱文件。ARM 提供了 Arm 架構的程序調用標準 - ABI 2019Q1ARM 64 位架構 (AArch64) 的程序調用標準。但是,x86 和 x64 的官方文件 從網路上消失了,導致人們搜尋這些資訊,並求助於非官方的鏡像或 逆向工程

快速瀏覽文件會顯示傳值結構體的各種位置:

  • 在多個 CPU 和 FPU 暫存器中。
  • 在堆疊上。
  • 指向副本的指標。(副本位於調用者的堆疊框架上。)
  • 部分在 CPU 暫存器中,部分在堆疊上。

當在堆疊上传遞時,還有一些關於所需對齊方式以及所有未使用的 CPU 和 FPU 暫存器是否被阻塞或回填的進一步問題。

當返回傳值結構體時,結構體可以在兩個位置傳回:

  • 在多個 CPU 和 FPU 暫存器中。
  • 由被調用者寫入記憶體位置,在這種情況下,調用者傳遞指向該記憶體位置的指標。(此預留記憶體也在調用者的堆疊框架上。)

當傳遞指向結果位置的指標時,一個進一步的問題是,這是否與普通的 CPU 引數暫存器衝突。

重構 Dart FFI 編譯

初步調查就足以讓我們意識到,我們必須重新設計 Dart FFI 編譯器管道的一部分。我們曾經重複使用 Location 類型,該類型最初是用於將 Dart 程式碼編譯為組合語言的。

但是,在 Dart ABI 中,我們從不使用非字對齊的堆疊位置或同時使用兩個以上的暫存器。一個嘗試擴充 Location 類型以支援這些額外位置的實驗最終導致了一個巨大的複雜差異,因為 Location 在 Dart 虛擬機器中被大量使用。

因此,我們 替換了 Dart FFI 的編譯管道

探索原生 ABI

讓我們來探索一下 ABI。

假設我們有以下結構體和 C 函數簽名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Struct3Bytes {
int8_t a;
int16_t b;
};

struct Struct3Bytes MyFunction(
struct Struct3Bytes,
struct Struct3Bytes,
struct Struct3Bytes,
struct Struct3Bytes,
struct Struct3Bytes,
struct Struct3Bytes,
struct Struct3Bytes,
struct Struct3Bytes);

各種 ABI 如何在 MyFunction 中傳遞這些結構體?

在 x64 上的 Linux 中,有 6 個 CPU 引數暫存器。結構體足夠小,可以放入單個暫存器中,因此前 6 個引數進入 6 個 CPU 引數暫存器,最後 2 個進入堆疊。堆疊引數以 8 位元組對齊。並且,返回值也適合放入 CPU 暫存器中(更大的範例)。

1
2
3
mov     rdi, rsi
mov rdx, rcx
...

那麼,在 Windows 上會發生什麼?

完全不同。Windows 只有 4 個引數暫存器。但是,第一個暫存器用於傳遞指向要將返回值寫入的記憶體位置的指標。並且,所有引數都透過指向副本的指標傳遞,因為結構體的大小為 3 位元組,這不是 2 的冪。

1
2
3
mov     qword ptr [rsp+32], r9  ; arg8
mov qword ptr [rsp+24], r8 ; arg7
...

讓我們看看另一個範例:Linux 和 Android 上的 ARM32。假設我們有以下結構體和 C 函數簽名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Struct16BytesHomogenousFloat {
float a0;
float a1;
float a2;
float a3;
};

struct Struct16BytesHomogenousFloat MyFunction(
struct Struct16BytesHomogenousFloat,
struct Struct16BytesHomogenousFloat,
struct Struct16BytesHomogenousFloat,
struct Struct16BytesHomogenousFloat,
struct Struct16BytesHomogenousFloat,
struct Struct16BytesHomogenousFloat,
struct Struct16BytesHomogenousFloat,
struct Struct16BytesHomogenousFloat,
struct Struct16BytesHomogenousFloat,
struct Struct16BytesHomogenousFloat);

這些特定類型的結構體稱為同類複合類型,因為它們只包含相同的元素。並且,具有最多 4 個成員的同類浮點數的處理方式與普通結構體不同。在這種情況下,Linux 對結構體中的各個浮點數使用浮點暫存器

1
2
3
vmov            s1, r2
vmov s0, r1
...

在 Android 上,使用 SoftFP 而不是 HardFP。這表示浮點數在整數暫存器中傳遞,而不是在浮點暫存器中傳遞。此外,我們正在傳遞一個指向結果的指標。這導致了一種 奇怪的情況,其中第一個引數部分在整數暫存器中傳遞,部分在堆疊上传遞。

1
2
3
str             r2, [r0, #4]
str r1, [r0]
...

如果其中任何一個出錯,都可能導致執行階段出現區段錯誤。因此,在每個硬體和作業系統組合上正確處理 ABI 的所有邊角案例至關重要。

透過 godbolt.org 探索

由於文件非常簡潔,我們透過編譯器資源管理器 godbolt.org 找出許多邊角案例。編譯器資源管理器 並排顯示 C 程式碼和編譯後的組合語言:

A screenshot of godbolt.com showing that the assembly code for sizeof(Struct3Bytes) is returning 3 in the return register.

上述螢幕截圖顯示,在 Windows x86 上,sizeof(Struct3Bytes) 為 3 位元組,因為 3 被移入返回暫存器 eax 中。

當我們 稍微變更 結構體時,我們可以檢查大小是否仍然為 3:

1
2
3
4
struct Struct3Bytes {
uint8_t a;
int16_t b;
};

大小不是 3:mov eax, 4。由於 int16 必須為 2 位元組對齊,因此結構體必須為 2 位元組對齊。這表示,在配置這些結構體的陣列時,每個結構體之後有一個 1 位元組的填補,以確保下一個結構體為 2 位元組對齊。因此,此結構體在本機 ABI 中為 4 位元組。

透過產生的測試探索

不幸的是,編譯器資源管理器不支援 MacOS 和 iOS。因此,為了使手動探索更有效率(並且為此功能提供一個良好且龐大的測試套件),我們編寫了一個測試產生器。

主要思想是以這樣的方式產生測試:如果測試崩潰,則可以使用 GDB 來查看問題所在。

使發現 segmentation fault 時更容易看到問題的一種方法是使所有引數都具有可預測且易於識別的值。例如,以下測試使用連續的整數,以便可以在暫存器和堆疊上輕鬆發現這些整數值:

1
2
3
4
5
6
7
8
9
struct Struct8Bytes {
int16_t a;
int16_t b;
int32_t c;
};

int main() {
struct Struct8Bytes s = {1, 2, 3};
}

另一種簡化尋找問題的方法是在各處加入列印語句。例如,如果我們在從 Dart 轉換到 C 語言的過程中沒有遇到 segmentation fault,但我們設法損壞了所有引數,則列印引數會有所幫助:

1
2
3
4
5
6
7
8
struct Struct1ByteInt8 {
int8_t a0;
};

void MyFunction(struct Struct1ByteInt8 a0, int8_t a1) {
printf("%" PRId8 "\n", a0.a0);
printf("%" PRId8 "\n", a1);
}

加入測試就像在 組態檔案 中加入函數類型一樣簡單。快速加入測試的能力導致了一個 龐大的測試套件

果然,這個測試套件在本機 ABI 中發現了另一個奇怪的案例 - 這次是在 iOS-ARM64 上。在 ARM64 上的 iOS 上,堆疊上的非結構體引數不是以字大小對齊,而是以其自身的大小對齊。結構體以字大小對齊,除非結構體是只包含浮點數的同類結構體,則 它以浮點數的大小對齊

總結

到此結束了我們對 API 設計和 ABI 探索的旅程。透過良好的測試套件和全面的程式碼審查,我們在 2020 年 12 月在 master 分支上 加入了對在 Dart FFI 中傳遞傳值結構體的支援,並且它在 Dart 2.12 中可用!如果您有興趣使用 Dart FFI,則可以從 dart.dev 上的 C 語言互通文件 開始。如果您對 API 設計和 ABI 探索有任何問題或意見,請在下方留言。我們很想聽到您的聲音!

感謝 Dart 語言團隊和(其餘的)Dart 虛擬機器團隊對此 Dart FFI 功能的貢獻,也感謝 Kathy Walrath 和 Michael Thomsen 對此部落格文章的塑造!


在 Dart FFI 中實作傳值結構體 最初發佈在 Dart 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

【文章翻譯】Announcing Dart 2.13

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

宣佈 Dart 2.13

A graphic showing highlights of what’s in this post: type aliases, better FFI, null safety, Docker support

作者:Kevin Moore 和 Michael Thomsen

今天,我們宣佈推出 Dart 2.13,其中包含類型別名——目前我們第二個最受歡迎的語言功能。Dart 2.13 還包含改進的 Dart FFI 和更好的效能,並且我們有新的 Dart 官方 Docker 映像。這篇文章提供了在 2.12 中引入的空安全功能的更新,討論了新的 2.13 功能,有一些關於 Docker 和 Google Cloud 支援 Dart 後端的令人興奮的消息,並預覽了一些您可以在未來版本中看到的一些變更。

空安全更新

我們在 3 月的 Dart 2.12 版本中推出了健全的空安全。空安全是 Dart 最新的主要生產力功能,旨在幫助您避免空錯誤——這類錯誤通常難以發現。隨著該版本的推出,我們鼓勵套件發佈者開始將 pub.dev 上的共享套件遷移到空安全。

我們非常高興地看到空安全被採用的速度如此之快!在發佈後的短短幾個月內,pub.dev 上前 500 個最受歡迎的套件中,有 93% 已經支援空安全。我們要向所有套件開發人員表示衷心的感謝,感謝他們如此迅速地完成了這項工作,並幫助整個生態系統向前發展!

有了這麼多支援空安全的套件,您很有可能可以開始將您的應用程式遷移到使用空安全。第一步是使用 dart pub outdated 檢查您的應用程式的相依性。有關詳細資訊,請參閱空安全遷移指南。我們還更改了我們的 dart createflutter create 範本,以便它們現在預設在新應用程式和套件中啟用空安全。

宣佈類型別名

類型別名是 2.13 語言中的一項新功能。它擴展了我們早期的支援,允許建立函數類型的類型別名,但不支援任何其他類型。這個備受期待的功能在語言問題追蹤器中被評為第二高

使用類型別名,您可以為任何現有類型建立新名稱,然後可以在任何可以使用原始類型的地方使用該名稱。您並不是真的在定義一個新類型,只是引入了一個簡寫別名。別名甚至通過類型相等測試:

1
2
3
4
5
typedef Integer = int;

void main() {
print(int == Integer); // true
}

那麼您可以使用類型別名做什麼呢?一種常見的用法是為類型指定一個更短或更具描述性的名稱,使您的程式碼更具可讀性和可維護性。

一個很好的例子是使用 JSON(感謝 GitHub 使用者 Levi-Lesches 提供的這個例子)。在這裡,我們可以定義一個新的類型別名 Json,它將 JSON 文件描述為從 String 鍵到任何值(使用動態類型)的映射。然後,我們可以在定義我們的 fromJson 命名建構函數和 json getter 時使用該 Json 類型別名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef Json = Map<String, dynamic>;

class User {
final String name;
final int age;

User.fromJson(Json json) :
name = json['name'],
age = json['age'];

Json get json => {
'name': name,
'age': age,
};
}

您也可以在命名類別的類型別名上調用建構函數,因此以下是完全合法的:

1
2
3
4
main() {
var j = Json();
j['name'] = 'Michael';
}

透過使用類型別名為複雜類型命名,您可以使讀者更容易理解程式碼的不變性。例如,以下程式碼定義了一個類型別名來描述包含泛型類型 X 的鍵和 List<X> 類型值的映射。透過為類型指定一個帶有單個類型參數的名稱,映射的規則結構對於程式碼的讀者來說變得更加明顯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef MapToList<X> = Map<X, List<X>>;

void main() {
MapToList<int> m = {};
m[7] = [7]; // OK
m[8] = [2, 2, 2]; // OK
for (var x in m.keys) {
print('$x --> ${m[x]}');
}
}

=>

7 --> [7]
8 --> [2, 2, 2]

如果您嘗試使用不匹配的類型,您將收到分析錯誤:

1
2
3
4
5
m[42] = ['The', 'meaning', 'of', 'life'];

=>

元素類型 'String' 無法賦值給列表類型 'int'

您甚至可以在重命名公共程式庫中的類別時使用類型別名。想像一下,您在公共程式庫中有一個現有的類別 PoorlyNamedClass,您想將其重命名為 BetterNamedClass。如果您只是重命名類別,那麼您的 API 客戶將會收到突然的編譯錯誤。使用類型別名,您可以繼續進行重命名,但然後為舊的類別名稱定義一個新的類型別名,然後為舊名稱添加一個 @Deprecated 註釋。使用 PoorlyNamedClass 時會導致警告,但會繼續編譯並像以前一樣工作,讓使用者有時間升級他們的程式碼。

以下是您如何在 mylibrary.dart 檔案中實作 BetterNamedClass 並棄用 PoorlyNamedClass

1
2
3
4
class BetterNamedClass {...}

@Deprecated('請改用 BetterNamedClass')
typedef PoorlyNamedClass = BetterNamedClass;

以下是當有人嘗試使用 PoorlyNamedClass 時會發生的情況:

1
2
3
4
5
6
7
8
9
import 'mylibrary.dart';

void main() {
PoorlyNamedClass p;
}

=>

'PoorlyNamedClass' 已棄用,不應使用。請改用 BetterNamedClass。

類型別名功能從 Dart 2.13 開始提供。要啟用它,請將 pubspec 中的 Dart SDK 下限約束設定為至少 2.13:

1
2
environment:
sdk: ">=2.13.0 <3.0.0"

由於語言版本控制,此功能向後相容。SDK 約束低於 2.13 的套件可以安全地引用在 2.13 套件中定義的類型別名,即使 2.13 之前的套件無法定義自己的類型別名。

Dart 2.13 FFI 變更

我們在 Dart FFI(我們用於調用 C 程式碼的互操作機制)中也有一些新功能。

首先,FFI 現在支援具有內聯陣列的結構體(#35763)。考慮一個具有內聯陣列的 C 結構體,如下所示:

1
2
3
struct MyStruct {
uint8_t arr[8];
}

您現在可以直接在 Dart 中包裝它,使用 Array 的類型參數指定元素類型:

1
2
3
4
class StructInlineArray extends Struct {
@Array(8)
external Array<Uint8> arr;
}

其次,FFI 現在支援壓縮結構體(#38158)。通常,結構體在記憶體中的佈局方式使得成員位於更容易被 CPU 存取的地址邊界中。使用壓縮結構體,可以省略一些填充以降低整體記憶體消耗,通常以特定於平台的方式。使用新的 @Packed(<alignment>) 註釋,您可以輕鬆指定填充。例如,以下程式碼建立了一個在記憶體中具有 4 位元組對齊的結構體:

1
2
3
4
5
6
7
8
9
10
11
12
@Packed(4)
class TASKDIALOGCONFIG extends Struct {
@Uint32()
external int cbSize;
@IntPtr()
external int hwndParent;
@IntPtr()
external int hInstance;
@Uint32()
external int dwFlags;
...
}

Dart 2.13 效能變更

我們正在繼續努力減少 Dart 程式碼的應用程式大小和記憶體佔用。在大型 Flutter 應用程式中,表示 AOT 編譯的 Dart 程式的中繼資料的內部結構可能會佔用相當大的記憶體塊。這些中繼資料大多數都用於啟用熱重新載入、互動式除錯和人類可讀的堆疊追蹤格式等功能——這些功能在已部署的應用程式中從未使用過。在過去的一年中,我們一直在重構 Dart 原生運行時,以盡可能消除這種開銷。其中一些改進適用於以發佈模式構建的所有 Flutter 應用程式,但有些改進需要您透過使用 --split-debug-info 旗標將除錯資訊從 AOT 編譯的應用程式中分離出來,以放棄人類可讀的堆疊追蹤。

Dart 2.13 包含許多變更,這些變更在使用 --split-debug-info 時顯著減少了程式中繼資料佔用的空間。以 Flutter Gallery 應用程式為例。在 Android 上,發行版 APK 的大小為 112.4 MB(包含除錯資訊),106.7 MB(不包含除錯資訊)(總體減少了 5%)。此 APK 包含許多資產。僅查看 APK 內的程式碼中繼資料,它從 Dart 2.12 的 5.7 MB 減少到 Dart 2.13 的 3.7 MB(減少了 35%)。

如果應用程式大小和記憶體佔用對您很重要,請考慮使用 --split-debug-info 旗標省略除錯資訊。請注意,這樣做的話,您需要使用符號化命令使堆疊追蹤再次可讀。

官方 Docker 支援和 Google Cloud 上的 Dart

Dart 現在可以作為Docker 官方映像使用。雖然 Dart 多年來一直提供 Docker 映像,但這些新的 Dart 映像已通過 Docker 的測試和驗證,符合最佳實務。它們還支援提前 (AOT) 編譯,這可以顯著減少構建的容器的大小,並可以提高在容器環境(例如Cloud Run)中的部署速度。

雖然 Dart 仍然專注於讓 Flutter 等應用程式框架能夠在每個螢幕上驅動漂亮的像素,但我們意識到,大多數使用者體驗背後至少有一個託管服務。透過簡化使用 Dart 構建後端服務,我們支援完整的堆疊體驗,讓開發人員可以使用與他們用於在前段驅動 widget 的相同語言和業務邏輯將他們的應用程式擴展到雲端。

一般來說,將 Dart 用於 Flutter 應用程式後端尤其適合 Google 的託管無伺服器平台 Cloud Run 的簡潔性和可擴展性。這包括縮放為零,這意味著當您的後端沒有處理任何請求時,您不會產生成本。我們與 Google Cloud 團隊合作,提供Dart 函數框架,這是一個套件、工具和範例的集合,可以輕鬆編寫 Dart 函數來部署,而不是完整的伺服器來處理 HTTP 請求和 CloudEvents。

請查看我們的Google Cloud 文件以開始使用。

關於接下來的一些話

我們已經在為即將發佈的版本進行一些令人興奮的變更。與往常一樣,您可以使用語言漏斗追蹤器來關注我們的進度。

我們正在研究的一個領域是 Dart 和 Flutter 的一套新的規範 lint。Lint 是一種配置 Dart 靜態分析的強大方法,但由於有數百個可能的 lint 可以開啟或關閉,因此很難決定選擇什麼。我們目前正在定義兩套規範的 lint,我們將在 Dart 和 Flutter 專案中預設應用這些 lint。我們預計這將在下一個穩定版本中預設啟用。如果您想要預覽,請查看兩個套件 lintsflutter_lints

最後,如果您進行 Dart VM 運行時的深度嵌入,請注意,我們計劃棄用現有的機制。我們將用一個基於 Dart FFI 的更快、更靈活的模型來替換它(請參閱追蹤問題 #45451)。

Dart 2.13 現已推出

Dart 2.13,包含類型別名和改進的 FFI,現已在 Dart 2.13Flutter 2.2 SDK 中推出。

如果您一直在等待您的相依性遷移到空安全,您可能需要再次檢查,使用 dart pub outdated。前 500 個最受歡迎的套件中,有 93% 已經遷移,您很有可能已經沒有障礙了。我們還要向已經遷移的開發人員表示衷心的感謝!

我們很樂意聽到您對本部落格文章中討論的新功能和變更的體驗。請在下方留言或在 Twitter 上聯繫我們 @dart_lang


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