0%

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