【文章翻譯】Raster thread performance optimization tips

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

柵格執行緒效能優化技巧

最近,我坐下來調整 FlutterFolio 的效能,這個應用程式是作為 Flutter Engage 的設計展示而建立的。只改變了一處,我就讓 FlutterFolio 變得快了很多。

不過,首先,我必須找出要改什麼。這篇文章就是關於這個搜尋的。

FlutterFolio 是一個功能完整的應用程式,從設計到實作,花了 6 個星期 (!) 時間完成,支援行動裝置、桌面和網頁。開發團隊顯然必須做一些妥協 - 沒有任何批評。專案的範圍和非常短的時程迫使他們這樣做。

事實上,這是一個絕佳的機會,因為這個應用程式比我想到的所有範例應用程式都更加「真實」。

而且,效能優化在真實應用程式上比在合成問題上更好解釋。

第 1 步:效能分析

任何優化的第一步是什麼?量測。知道一個應用程式看起來很慢是不夠的。你需要更精確一點。兩個原因:

  1. 量測可以將我們引導到最糟糕的肇事者。任何應用程式的每個部分都可以變得更快、更有效率。但是,你必須從某個地方開始。效能分析讓我們看到哪些部分做得還可以,哪些部分做得不好。然後,我們可以專注於做得不好的部分,並在有限的時間內取得更大的進展。
  2. 我們可以比較前後。有時,程式碼變更看起來像是個好主意,但實際上,它並沒有產生顯著的差異。有了基準(之前)就表示我們可以量化變更的影響。我們可以將之前與之後進行比較。

應用程式的效能分析很困難。我在 2019 年寫了一篇關於這方面的 長篇文章。所以,讓我們從簡單的開始。我們在 profile 模式下執行應用程式,開啟效能覆蓋圖,並使用應用程式,同時觀察效能覆蓋圖的圖表。

我們立即看到柵格執行緒正在努力。

這尤其發生在捲軸瀏覽應用程式的主頁時。您應該始終優先考慮使用者花費大部分時間的部分或效能問題對使用者特別明顯的部分。換句話說,如果您有兩個效能問題,其中一個發生在開始畫面,另一個埋藏在設定頁面中,請先修復開始畫面。

讓我們看看柵格執行緒在做什麼。

旁白:UI 執行緒 vs. 柵格執行緒

其實,讓我們先澄清柵格執行緒做什麼。

所有 Flutter 應用程式都在至少兩個並行執行緒上運行:UI 執行緒和柵格執行緒。UI 執行緒是您建立 Widget 和應用程式邏輯運行的地方。(您可以建立隔離區,這表示您可以在其他執行緒上運行邏輯,但為了簡單起見,我們將忽略這一點。)柵格執行緒是 Flutter 用來 柵格化 您的應用程式的方式。它接收來自 UI 執行緒的指令,並將它們轉換成可以傳送到圖形卡的東西。

為了更具體,讓我們看看一個 build 函數:

1
2
3
Widget build(BuildContext context) {
return Image.asset('dash.png');
}

上面的程式碼在 UI 執行緒上運行。Flutter 架構找出要放置 Widget 的位置、要給它什麼尺寸,等等 - 仍然在 UI 執行緒上。

然後,在 Flutter 知道有關畫面的所有資訊之後,它就會交給柵格執行緒。柵格執行緒會取得 dash.png 中的位元組,調整圖片的大小(如果需要),然後應用不透明度、混合模式、模糊等等,直到它取得最終的像素。然後,柵格執行緒將得到的資訊傳送到圖形卡,因此也傳送到螢幕。

第 2 步:深入時間軸

好的,回到 FlutterFolio。開啟 Flutter DevTools 讓我們可以更仔細地查看時間軸。

效能 標籤中,您可以看到 UI 執行緒(淡藍色條)做得很好,而柵格執行緒(深藍色和紅色條)在每個畫面中花費了令人驚訝的時間,特別是在捲軸向下瀏覽主頁時。因此,問題不在於低效的 build 方法或商業邏輯。問題是要求柵格執行緒做太多事。

事實上,每個畫面 都花費了很長時間在柵格執行緒上,這告訴了我們一些事情。它表示我們要求柵格執行緒做一些工作 一次又一次 - 它不是偶爾做一次的事情。

讓我們選擇一個畫面,看看 時間軸事件 面板。

時間軸的頂部,帶有淺灰色背景,是 UI 執行緒。再次,您可以看到 UI 執行緒不是問題。

在 UI 執行緒下方,您可以看到柵格執行緒上的事件,從 GPURasterizer:Draw 開始。不幸的是,這就是事情變得有點模糊的地方。有許多對異國情調名稱方法的呼叫,例如 TransformLayer::Preroll、OpacityLayer::Preroll、PhysicalShapeLayer::Paint 等等。沒有關於這些方法中發生了什麼事情的詳細資訊,而且這些不是大多數 Flutter 開發人員認識的名稱。

它們是來自 Flutter 引擎的 C++ 方法。如果您想,您可以 搜尋 這些方法名稱,閱讀程式碼和註解,看看幕後發生了什麼。有時,這可以讓您對柵格執行緒在做什麼有更多直覺。但是,這種研究對於找出效能問題來說並不嚴格需要。(我直到相對最近才做過這種研究,但仍然能夠優化許多應用程式的效能。)

然後,有一個標記為 SkCanvas::Flush 的長事件。它花了 18 毫秒,遠遠超過合理範圍。不幸的是,該事件也沒有任何詳細資訊,因此我們需要做一點偵探工作。

SkCanvas 中的 Sk 代表 Skia,這是 Flutter 用於在其堆疊最底層進行渲染的圖形引擎。SkCanvas 是一個低階 C++ 類別,類似於 Flutter 自己 的 Canvas(如果您使用 CustomPaint,您可能已經熟悉它)。您應用程式的所有像素、線條、漸變 - 所有 UI - 都會經過 SkCanvas。而且,SkCanvas::Flush 是這個類別在收集到所有需要的資訊後進行大部分工作的地方。文件 指出 Flush 方法「解決所有未決的 GPU 作業」。

讓我們回顧一下我們從效能時間軸中学到的东西:

  • 柵格執行緒是主要問題。UI 執行緒做得相對良好。
  • 在捲軸時,柵格執行緒在 每個畫面 中花費了很長時間。一些昂貴的柵格化工作一直在進行。
  • SkCanvas::Flush 花費了很長時間,這表示 Skia 正在做很多功課。

我們 不知道 那個功課是什麼。讓我們回顧一下程式碼。

第 3 步:閱讀程式碼

有了知識,讓我們看看原始碼。如果程式碼不熟悉(就像我對 FlutterFolio 的情況一樣),最好從 profile 模式切換到 debug 模式,並使用 Flutter Inspector 跳轉到相關 Widget 的原始碼。

FlutterFolio 的主頁,至少在行動裝置上,似乎基本上是一個由 BookCoverWidgets 填充的垂直 PageView。查看 BookCoverWidget,您可以看到它本質上是一個 各種 Widget 的堆疊,從底部的大型圖片開始,繼續是一些動畫覆蓋層、主要文字內容,最後是頂部的滑鼠懸停覆蓋層。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
child: Stack(fit: StackFit.expand, children: [
/// /////////////////////////////<br>
/// 背景圖片
// 當我們滑鼠懸停時,動畫縮放
AnimatedScale(
duration: Times.slow,
begin: 1,
end: isClickable ? 1.1 : 1,
child: BookCoverImage(widget.data),
),

/// 黑色覆蓋層,在滑鼠懸停時淡出
AnimatedContainer(duration: Times.slow,
color: Colors.black.withOpacity(overlayOpacity)),

/// 當處於大型模式時,顯示一些漸變,
/// 應該位於文字元素下方
if (widget.largeMode) ...[
FadeInLeft(
duration: Times.slower,
child: _SideGradient(Colors.black),
),
FadeInUp(child: _BottomGradientLg(Colors.black))
] else ...[
FadeInUp(child: _BottomGradientSm(Colors.black)),
],

/// 放在文字內容下方,點擊時取消焦點。
GestureDetector(behavior: HitTestBehavior.translucent,
onTap: InputUtils.unFocus),

/// BookContent,顯示大型封面或小型封面
Align(
alignment: widget.topTitle ? Alignment.topLeft : Alignment.bottomLeft,
// 根據我們處於哪種模式,對填補進行動畫轉場
child: AnimatedContainer(
duration: Times.slow,
padding: EdgeInsets.all(widget.largeMode ? Insets.offset : Insets.sm),
child: (widget.largeMode)
? LargeBookCover(widget.data)
: SmallBookCover(widget.data, topTitle: widget.topTitle),
),
),

/// 滑鼠懸停效果
if (isClickable) ...[
Positioned.fill(child: FadeIn(child: RoundedBorder(color: theme.accent1, ignorePointer: false))),
],
]),

現在,請記住:您正在尋找的是在每個畫面中都會發生的東西(也就是說,它始終存在),並且可能對於 Skia 渲染器來說繪製起來很昂貴(圖片、模糊、混合等等)。

第 4 步:深入探討

現在,您需要深入探討以找出可能存在问题的 Widget。一種方法是暫時從應用程式中移除各種 Widget,看看這對效能有什麼影響。

請記住,堆疊的第一個子元素是背景,每個後續子元素都是之前 Widget 上方的圖層。所以,第一個子元素是背景圖片,由 BookCoverImage 表示。您可以移除它,但主頁看起來會像這樣:

這就失去了整個頁面的意義。仔細查看 BookCoverImage,您可以看到它只是一個簡單的 Image 函式包裝器。除了一个值得注意的例外(本文稍後會提到)之外,這裡沒有什麼可以改進的地方。

繼續,有這段程式碼:

1
2
3
/// 黑色覆蓋層,在滑鼠懸停時淡出
AnimatedContainer(duration: Times.slow,
color: Colors.black.withOpacity(overlayOpacity)),

這是一個用透明黑色覆蓋整個圖片的 Widget。overlayOpacity 預設情況下為 0(而且大部分時間都是如此),所以這個圖層是完全透明的。嗯。讓我們將它移除,並再次在 profile 模式下執行應用程式。

有趣!柵格執行緒仍然承擔了大量負載,但效能有了顯著改善。

我決定為 FlutterFolio 實作一個更強大的效能分析工具,以便我可以證明改進是真實的,而不仅仅是偶然。這個變更讓我柵格化所花費的 CPU 時間整體減少了 20%,潛在的卡頓減少了 50%。

總的來說,這是一個巨大的變化,因為移除了一個大部分時間都無效的單個 Widget。

修復 很簡單:

1
2
3
4
/// 黑色覆蓋層,在滑鼠懸停時淡出
if (overlayOpacity > 0)
AnimatedContainer(duration: Times.slow,
color: Colors.black.withOpacity(overlayOpacity)),

現在,您只在它具有非零不透明度(也就是說,它至少部分可見)時才會加入透明覆蓋層。您避免了(非常常見的!)情況,即建立並柵格化一個完全透明的圖層,但它没有任何效果。

就這樣,應用程式變得更加流暢,也更節省電量。

注意:為什麼你需要這樣做?Flutter 不應該足夠聰明地為我們執行這個優化嗎?請閱讀 此處 的議題,了解為什麼它做不到。為什麼透明不透明度在第一時間會變慢?這超出了本文的範圍,但它與堆疊中更上方的 BackdropFilter Widget 相關,它會與下方每個 Widget 相互作用。

本文的主要目的不是教你關於這個特定效能陷阱的知識。你可能永遠不會再看到它。我的目標是教你如何一般性地優化柵格執行緒效能。

第 5 步:概括

在繼續處理一個完全不同的問題之前,通常最好查看專案中的其他地方,看看是否有類似的問題。我們的應用程式中是否有其他地方使用了大面積覆蓋層?你能避免它們嗎?

在這種情況下,接下來的幾行會建立在您捲軸時會淡入的大型漸變:

1
2
3
4
5
6
7
8
9
10
11
/// 當處於大型模式時,顯示一些漸變,
/// 應該位於文字元素下方
if (widget.largeMode) ...[
FadeInLeft(
duration: Times.slower,
child: _SideGradient(Colors.black),
),
FadeInUp(child: _BottomGradientLg(Colors.black))
] else ...[
FadeInUp(child: _BottomGradientSm(Colors.black)),
],

而且,果然,移除這些動畫的、幾乎佔據全螢幕的漸變會顯著改善捲軸效能。不幸的是,在這種情況下,解決方案不像之前的範例那样简单。這些漸變不是無形的。它們會在使用者到達特定封面時開始淡入。移除它们 确实 会造成視覺上的差異。

一個想法是稍微延遲淡入,這樣動畫只会在使用者降落在特定 BookCover 上時才會開始。這樣一來,您就可以減輕柵格執行緒的負載,而使用者在捲軸時,也希望可以避免一些潛在的卡頓。

但是,這涉及到對應用程式的動態設計進行修改,因此需要與更廣泛的團隊討論。許多效能優化都會屬於這類。效能優化通常是折衷的過程。

重複步驟 2-5 直到滿意

到目前為止,我們只查看了一種類型的問題。總是會有更多問題。

以下是一個關於下一步的想法:應用程式的圖片資產是否太大?請記住,柵格執行緒負責取得圖片位元組、解碼它們、調整大小、應用濾鏡等等。如果它將一個 20 MB 的高解析度圖片載入到螢幕上的一個小頭像圖片中,那麼您就是在浪費資源。

當您的應用程式在 debug 模式下執行時,您可以使用 Flutter Inspector 來 反轉過大的圖片

這將會反轉顏色並翻轉應用程式中對於實際用途來說太大的所有圖片。然后,您可以仔细检查应用程序,并注意不自然的变化。

debug 模式還會在每次遇到這種圖片時報告一個錯誤,例如:

[錯誤] 圖片 assets/images/empty-background.png 的顯示大小為 411×706,但解碼大小為 2560×1600,額外使用了 19818KB。

不過,這裡的解決方案並不直接。在行動裝置上,您不需要 2560×1600 的圖片,但在桌面上,您可能需要。請記住,FlutterFolio 是一個在所有 Flutter 目標上執行的應用程式,包括桌面。如有疑問,請 閱讀 API 文件

結語

如您所見,優化效能是一门艺术和科学。強大的基準測試以及對框架及其內建 Widget 的深刻理解都有助於這一點。

最终,熟能生巧。優化足夠多的應用程式,你就會變得更好。

祝您狩獵愉快。


柵格執行緒效能優化技巧 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。

https://creativecommons.org/licenses/by-nc/4.0/

【文章翻譯】Writing a good code sample

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

寫一個好的程式碼範例

撰寫好的程式碼範例很難。讓我說明一下,製作一個範例來演示 API 的用法或展示 UI 慣用語可能很快且容易。難度來自於你需要添加到你的儲存庫中的額外部分,以保持程式碼範例新鮮並吸引你的開發人員。

首先,你需要在你的程式碼中添加一個好的 README,從一個摘要開始,說明開發人員為什麼應該投入時間和精力來研究和理解這個範例。接著,提供使用說明和連結到問題追蹤器,開發人員可以在其中提出錯誤並要求澄清,如果遇到令人困惑的部分。

接下來是程式碼本身。程式碼是否遵循佈局和使用的慣例?這可能像程式碼放在哪裡一樣簡單,也可能像 linter 等適當的設定一樣複雜。我喜歡已經採用了強大的程式碼格式、佈局和 linting 規則集標準的程式設計社群語言。這些標準使程式碼的外觀保持一致,從而更容易導航。若要強制執行 Dart 和 Flutter 的程式碼格式,你可以將以下命令添加到你的 CI 管線,如果格式不正確,則會使構建失敗:

1
$ dart format --output none --set-exit-if-changed .

在程式碼格式化後,下一步是強制執行一套好的 lints。對於 Dart,我強烈建議研究 lints 套件,對於 Flutter,我同樣建議研究 flutter_lints 套件。若要確保 lints 在 CI 管線中通過,請添加以下命令:

1
$ dart analyze

測試。哦,這麼多測試。單元測試、整合測試,對於 Flutter,我們還有 Widget 測試。測試對於範例來說非常棒,因為測試傳達了如何使用程式碼片段的意圖。測試還與前面提到的 CI 管線結合使用,可以使程式碼庫保持常青。想要了解更多關於 Flutter 測試功能的資訊,可以到 如何測試 Flutter 應用程式 codelab 了解更多。Dart 和 Flutter 的 CI 命令分別為:

1
2
$ flutter test   # 適用於 Flutter 專案
$ dart test # 適用於純 Dart 專案

如果你的程式碼儲存庫託管在 GitHub 上,那麼我建議你如果擁有 Dart 專案,請使用 Dart Setup 操作,如果擁有 Flutter 專案,請使用 Flutter Action。為了獲得額外積分,請考慮添加 Very Good Coverage 操作以保持高測試覆蓋率。既然你已經做到了這一步,你可能應該使用 工作流程狀態徽章 來宣傳你的 CI 狀態。

希望以上添加的項目清單能夠指導你,讓你的程式碼範例對你的目標受眾更有價值。


Writing a good code sample was originally published in Flutter on Medium, where people are continuing the conversation by highlighting and responding to this story.

【文章翻譯】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,人們在那裡透過突出顯示和回應這個故事來繼續討論。