Dart 2.15 現已發佈

作者 / Michael Thomsen, Dart & Flutter Product Manager, Google

我們已經正式發佈了 Dart SDK 的 2.15 版本,該版本新增了可快速並發的工作器 isolate、新的構造函數拆分 (tear-off) 語言特性、經過改進的 dart:core 庫枚舉支持、package 發佈者相關的新功能,等等。

工作器 isolate 的快速並發

如今,幾乎所有現代設備都使用多核 CPU,可以並行執行多個任務。對於大多數 Dart 程序來說,這些內核的使用情況對開發者而言是透明的: 默認情況下,Dart 運行時系統在單個內核上運行所有的 Dart 代碼,不過會使用其他內核來執行系統級任務,比如異步輸入/輸出,包括寫入文件或者調用網絡等。

不過您自己的 Dart 代碼可能也需要並發運行。例如,您可能需要展示一個連續的動畫,同時執行一個長時間運行的任務,比如解析一個大型 JSON 文件。如果額外任務花了太長時間,就可能會導致界面卡頓或延遲。如果將這些額外的任務移動到另一個單獨的內核,動畫就可以在主執行線程上繼續運行而不受干擾。

Dart 的並發模型基於 isolate,isolate 是一種相互隔離的獨立執行單元,這是為了避免出現與共享內存相關的大量並發編程錯誤,如 數據爭用等競態條件。Dart 通過禁止在 isolate 之間共享任何可變對象來避免這些錯誤,並使用 消息傳遞 在 isolate 之間交換狀態。在 Dart 2.15 中,我們對 isolate 進行了許多實質性的改進。

我們首先重新設計和實現了 isolate 的工作方式,引入了一個新概念: isolate 組。Isolate 組中的 isolate 共享各種內部數據結構,這些數據結構則表示正在運行的程序。這使得組中的單個 isolate 變得更加輕便。如今,因為不需要初始化程序結構,在現有 isolate 組中啟動額外的 isolate 比之前快 100 多倍,並且產生的 isolate 所消耗的內存減少了 10 至 100 倍。

雖然 isolate 組仍然阻止在 isolate 間共享訪問可變對象,但由於 isolate 組使用共享堆實現,這也讓其擁有了更多的功能。我們可以將對象從一個 isolate 傳遞到另一個 isolate,這可用於執行返回大量內存數據的任務的工作器 isolate。例如,工作器 isolate 通過網絡調用獲得數據,將該數據解析為大型 JSON 對象圖,然後將這個 JSON 圖返回到主 isolate 中。在推出 Dart 2.15 之前,執行該操作需要深度複製,如果複製花費的時間超過幀預算時間,就會導致界面卡頓。

在 Dart 2.15 中,工作器 isolate 可以調用 Isolate.exit(),將其結果作為參數傳遞。然後,Dart 運行時將包含結果的內存數據從工作器 isolate 傳遞到主 isolate 中,無需複製,且主 isolate 可以在固定時間內接收結果。我們已經在 Flutter 2.8 中更新了 compute() 實用函數,來利用 Isolate.exit()。如果您已經在使用 compute(),那麼在升級到 Flutter 2.8 後,您將自動獲得這些性能提升。

最後,我們還重新設計了 isolate 消息傳遞機制的實現方式,使得中小型消息的傳遞速度提高了大約 8 倍。發送消息的速度明顯更快,而接收信息幾乎總是在恆定的時間內完成。另外,我們擴展了 isolate 可以相互發送的對象種類,增加了對函數類型、閉包和堆棧跟蹤對象的支持。請參閱 SendPort.send() 的 API 文檔了解詳情。

要了解有關如何使用 isolate 的更多信息,請參閱我們為 Dart 2.15 添加的官方文檔 Dart 中的並發,以及更多 代碼示例

新語言特性: 構造函數拆分

在 Dart 中,您可以使用函數名稱創建一個函數對象,該對象指向另一個對象的函數。在以下示例中,main() 方法的第二行演示了將 g 指向 m.greet 的語法:

class Greeter {
  final String name;
  Greeter(this.name);

  void greet(String who) {
    print('$name says: Hello $who!');
  }
}
void main() {
  final m = Greeter('Michael');
  final g = m.greet; // g holds a function pointer to m.greet.
  g('Leaf'); // Invokes and prints "Michael says: Hello Leaf!"
}

在使用 Dart 核心庫時,這種函數指針 (也被稱為函數拆分) 經常出現。下面是通過傳遞函數指針在 iterable 上調用 foreach() 的示例:

final m = Greeter('Michael');
['Lasse', 'Bob', 'Erik'].forEach(m.greet);
// Prints "Michael says: Hello Lasse!", "Michael says: Hello Bob!",
// "Michael says: Hello Erik!"

在之前的版本中,Dart SDK 不支持創建構造函數的拆分 (語言問題 #216)。這就有點煩人,因為在許多情況下,例如構建 Flutter 界面時,就需要用到構造函數的拆分。從 Dart 2.15 開始,我們支持這種語法。以下是構建包含三個 Text widget 的 Column widget 的示例,通過調用 .map() 將 Text 構造函數的拆分傳遞給 Column 的子項。

class FruitWidget extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return Column(
       children: ['Apple', 'Orange'].map(Text.new).toList());
 }
}

Text.newText 類的默認構造函數。您也可以引用命名構造函數,例如 .map(Text.rich)

相關語言變化

在實現構造函數拆分時,我們也藉此機會修復了現有的函數指針功能中的一些不一致問題。現在可以特化泛型方法來創建非泛型方法:

T id<T>(T value) => value;
var intId = id<int>; // New in 2.15.
int Function(int) intId = id; // Pre-2.15 workaround.

您甚至可以特化一個泛型函數對象來創建一個非泛型函數對象:

const fo = id; // Tear off `id`, creating a function object.
const c1 = fo<int>; // New in 2.15; error before.

最後,Dart 2.15 清理了涉及泛型的類型字面量:

var y = List; // Already supported.
var z = List<int>; // New in 2.15.
var z = typeOf<List<int>>(); // Pre-2.15 workaround.

改進 dart:core 庫中的枚舉

我們為 dart:core 庫的枚舉 API 添加了許多優化 (語言問題 #1511)。現在您可以通過 .name 獲取每個枚舉值的 String 值:

enum MyEnum {
 one, two, three
}
void main() {
 print(MyEnum.one.name);  // Prints "one".
}

還可以按名稱查找枚舉值:

print(MyEnum.values.byName('two') == MyEnum.two);  // Prints "true".

最後,您可以獲得所有名稱-值對的映射:

final map = MyEnum.values.asNameMap();
print(map['three'] == MyEnum.three);  // Prints "true".

請參閱此 Flutter PR 查看這些新 API 的使用示例。

壓縮指針

Dart 2.15 增加了對壓縮指針的支持,這樣,如果只需要支持 32 位的地址空間 (最多 4 GB 內存),則 64 位 SDK 可以使用更加節省空間的指針表示形式。壓縮指針顯著減少了內存佔用,在對 Google Pay 應用的內部測試中,我們發現 Dart 堆的體積減少了大約 10%。

壓縮指針意味着無法處理 4 GB 以上的可用 RAM,因此該功能只存在於 Dart SDK 的配置選項中,只能在構建 SDK 時由 Dart SDK 的嵌入器啟用。Flutter SDK 2.8 版已為 Android 構建啟用此配置,Flutter 團隊正在考慮在後續版本中 為 iOS 構建啟用此配置

Dart SDK 中包含 Dart DevTools

以往 Dart SDK 不提供調試和性能工具的 DevTools 套件,您需要單獨下載。從 Dart 2.15 開始,下載 Dart SDK 時也會獲取 DevTools,無需進一步的安裝步驟。有關在 Dart 命令行應用中使用 DevTools 的更多信息,請參閱 DevTools 文檔

面向 package 發佈者的新 pub 功能

Dart 2.15 SDK 在 dart pub 開發者命令和 pub.dev package repo 中還新增了兩個功能。

首先,為 package 發佈者新增了一個安全功能,用於檢測發佈者在 pub package 中意外發佈 secret,例如 Cloud 或 CI 憑據。在了解到 GitHub repo 中 每天都有數以千計的 secret 被泄露後,我們便決定添加這個泄露檢測功能。

泄露檢測作為 dart pub publish 命令中的預發佈驗證的一部分運行。如果它在即將發佈的文件中檢測到潛在的 secret,publish 命令會退出,而不進行發佈,並打印如下輸出:

Publishing my_package 1.0.0 to //pub.dartlang.org:
Package validation found the following errors:
* line 1, column 1 of lib/key.pem: Potential leak of Private Key detected.
╷
1 │ ┌ - - -BEGIN PRIVATE KEY - - -
2 │ │ H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9LkJ
3 │ └ - - -END PRIVATE KEY - - -
╵
* line 2, column 23 of lib/my_package.dart: Potential leak of Google OAuth Refresh Token detected.
╷
2 │ final refreshToken = "1//042ys8uoFwZrkCgYIARAAGAQSNwF-L9IrXmFYE-sfKefSpoCnyqEcsHX97Y90KY-p8TPYPPnY2IPgRXdy0QeVw7URuF5u9oUeIF0";

在極少數情況下,此項檢測可能會出現誤報,將您實際上打算發佈的內容或文件標記為潛在泄露。在這些情況下,您可以將文件添加到 許可名單 中。

其次,我們還為發佈者添加了另一個功能: 撤銷已發佈的 package 版本。當發佈了有問題的 package 版本時,我們通常的建議是發佈一個小幅升級的新版本來修復意外問題。但在極少數情況下,例如您尚未修復這些問題,或是您在原打算只發佈一個次要版本時意外發佈了一個主要版本,那麼您就可以使用新的 package 撤銷功能,作為最後的補救方法。此功能在 pub.dev 的管理界面中提供:

在 package 版本被撤銷後,pub 客戶端在 pub getpub upgrade 中將不再解析該版本。如果有開發者已經解析該撤銷的版本 (並存在於他們的 pubspec.lock 文件中),他們將在下次運行 pub 時看到警告:

$ dart pub get
Resolving dependencies…
mypkg 0.0.181-buggy (retracted, 0.0.182-fixed available)
Got dependencies!

檢測雙向 Unicode 字符的安全性分析 (CVE-2021–22567)

最近發現了一個涉及雙向 Unicode 字符的通用編程語言漏洞 (CVE-2021–42574)。這個漏洞影響了大多數支持 Unicode 的現代編程語言。下面的 Dart 源代碼演示了這個問題:

main() {
 final accessLevel = 'user';
 if (accessLevel == 'user .⁦// Check if admin⁩ ⁦') {
   print('You are a regular user.');
 } else {
   print('You are an admin.');
 }
}

您可能會認為該程序會打印出 You are a regular user.,但實際上它打印出的是 You are an admin.!通過使用包含雙向 Unicode 字符的字符串,您就可能會造成這一漏洞。這些雙向字符針對在同一行的文本,可以將文本的方向由從左到右更改為從右到左,反之亦然。雙向字符文本在屏幕上的呈現與實際文本內容截然不同。您可以進一步查看此 GitHub gist 示例

針對此漏洞的緩解措施包括使用檢測雙向 Unicode 字符的工具 (編輯器、代碼審查工具等),以便開發者發現它們,並在知情的情況下使用這些字符。上面提到的 GitHub gist 文件查看器便是發現這些字符的工具的一個例子。

Dart 2.15 引入了進一步的緩解措施 (Dart 安全建議 CVE-2021–22567)。現在,Dart 分析器會掃描雙向 Unicode 字符,並標記對它們的任何使用:

$ dart analyze
Analyzing cvetest...                   2.6s
info • bin/cvetest.dart:4:27 • The Unicode code point 'U+202E'
      changes the appearance of text from how it's interpreted
      by the compiler. Try removing the code point or using the
      Unicode escape sequence '\u202E'. •
      text_direction_code_point_in_literal

我們建議用 Unicode 轉義序列替換這些字符,這樣它們就可在任何文本編輯器或查看器中顯示出來。或者,如果您確實正當使用了這些字符,您可以在使用這些字符的代碼行之前添加覆蓋語句來禁用警告:

// ignore: text_direction_code_point_in_literal

使用第三方 pub 服務器時的 pub.dev 憑據漏洞 (CVE-2021–22568)

我們也發佈了第二個與 pub.dev 相關的 Dart 安全建議: CVE-2021–22568。此建議針對可能將 package 發佈到第三方 pub package 服務器 (例如私人或公司內部 package 服務器) 的 package 發佈者。僅將 package 發佈到公開 pub.dev repo (標準配置) 的開發者 不受此漏洞的影響

如果您已經將 package 發佈至第三方 repo,那麼漏洞是: 用於在第三方 repo 進行身份驗證的 OAuth2 臨時 (一小時) 訪問令牌可能被誤用,以在公開 pub.dev repo 上進行身份驗證。因此惡意的第三方 pub 服務器可能會使用訪問令牌,在 pub.dev 上冒充您,並發佈 package。如果您已經將 package 發佈到一個不受信任的第三方 package repo,請考慮審查您的帳號在 pub.dev 公開 package repo 上的所有活動。我們推薦您使用 pub.dev 活動日誌 進行查看。

最後

希望您喜歡 已經推出 的 Dart 2.15 中的新功能。這是我們今年的最後一個版本,我們想藉此機會表達我們對美妙的 Dart 生態系統的感謝。感謝大家的寶貴反饋,以及對我們一直以來的支持,感謝大家在過去的一年中在 pub.dev 上發佈的數千個 package,它們豐富了我們的生態系統。我們迫切期待明年再次投入工作,我們計劃在 2022 年推出很多激動人心的內容。預祝大家新年快樂,好好享受即將到來的假期吧!