Flutter 多視窗最近進度,為什麼 3.44 還沒落地

本來以為 3.44 至少會釋出 Windows 的多視窗,結果只宣布了個 **Canonical 成為 Flutter Desktop 的主要維護者和策略合作夥伴** ,實際上很久前 Flutter 就把桌面端交給對方維護,這兩年也基本是這麼在推進,只是速度確實太慢:

這次更新裡主要提到了 showRawDialog / showDialog 走原生 dialog window ,如果你 flutter config --enable-windowing 開啟了 windowing,那麼 dialog 會透過 windowing system 顯示在自己的視窗,而不是目前視窗內的 modal overlay,平台不支援時,會 fallback 到一般 dialog route。

這個問題倒是把我坑了,因為我一直用的是 beta 版本,這次用了多視窗之後,很多 loading 和彈窗直接跑到新視窗上,然後又沒適配,直接 UX 都亂了

另外目前在介面層面,大部分 win 介面已經可用了:

  • RegularWindowController / RegularWindow
  • DialogWindowController / DialogWindow
  • TooltipWindowController / TooltipWindow
  • PopupWindowController / PopupWindow
  • SatelliteWindowController / SatelliteWindow

不過多視窗在 Flutter 裡確實挺多邊界問題,因為框架一開始就沒設計多視窗概念,所以很多東西都是在重構,現在的多視窗實作是一個 Flutter engine 實例能夠渲染多個獨立的作業系統視窗,而這些視窗共享同一個 Dart isolate 和 widget tree,然後透過 view ID 區分不同的渲染目標,所以多視窗之間的競態細節會比較多。

不過問題更多還是在於平台適配,從零開始做一套脫離平台渲染的多視窗,還真的難度不小。

Win

根據我體驗下來,Win 11 上基本都還行,用起來問題不大,就是多視窗焦點切換還有點互動支援不夠友善,但是在 Win 10 上目前問題應該比較多:

比如在一些 Win10 上,開關視窗、啟用、最大化 這些基礎功能在多視窗都還有問題,主要是 WM_SIZE 與等待機制在 Win10 上的行為不一致

目前 Flutter Win embedder 處理視窗大小變化(包括最大化、多視窗建立/關閉時的尺寸變化)的核心邏輯是:

  • 視窗收到 WM_SIZE 訊息(resize、maximize 等觸發)
  • embedder 的平台執行緒在 flutter_windows_view.cc 中執行一個 condition_variable + mutex ,逾時是 100ms,持續等待直到 Flutter engine 產出一幀與新目標尺寸匹配的畫面
  • 等待解除後,新畫面才真正呈現在螢幕上

這個等待機制是 Flutter Win 主要為了防止出現畫面撕裂和黑屏閃爍的,而問題就在這裡,Win10 在某些視窗操作(尤其是最大化,以及多視窗場景下的建立/啟用/關閉)時,WM_SIZE 訊息時序導致等待提前逾時或條件變數通知時序錯亂,跟 Win11 又居然不一樣,這就導致等待的觸發路徑跟不上了。

另外 Win 10 和 Win11 在視窗合成也有差異:

DWM(Desktop Window Manager)行為差異:

  • Win10/Win11 的 DWM、視窗合成策略、驅動模型和視窗樣式存在差異
  • Win 11 的視窗合成會更激進處理中間幀,視窗在內容好之前不會「破門而出」顯示給使用者
  • Win 10 的 DWM 對視窗顯示的同步更寬鬆,更容易暴露出黑畫面

WS_EX_LAYEREDWM_NCCALCSIZE 處理差異:

  • Flutter Win 建立視窗時使用的視窗樣式,在 Win 10 和 11 上對 WM_NCCALCSIZE 的處理有行為差異,會影響 resize 時的中間狀態是否可見

當然,更大的問題其實是多視窗場景放大了這些問題:

  • 單視窗時,應用啟動已經走過一遍這個路徑,即便有 bug 也只暴露一次
  • 多視窗時,每次新建/啟用/關閉視窗都會觸發上述路徑,Win 10 上每次操作都可能走到有問題的分支

另外還可能存在點擊問題,居然會有在第一幀 layout 完成之前就被分發進來的點擊事件,這些問題都是相當細節的具體場景,目前我整體體驗 Win11 還行,就是焦點切換還不太友善

macOS

另外 macOS 上也是類似,主要問題還是視窗在第一幀渲染完成之前就顯示了,而且問題在 Intel Mac(macOS 15.7.5)上可以穩定復現,在 M2 MacBook Pro(macOS 26.4)上又不穩定復現,不得不說,現在 macOS 的版本號碎片化也很豐富了。

那為什麼這個問題在 macOS 上也會發生?因為 macOS 的視窗顯示機制:

  • macOS 使用 NSWindowNSView,視窗建立後如果呼叫 makeKeyAndOrderFront:,視窗就會立即可見
  • Flutter macOS embedder 目前沒有實作「延遲顯示直到第一幀就緒」的機制

RegularWindowController 目前又缺少:

  • 隱藏/延遲建立能力:建立視窗但不立即顯示,等待就緒訊號

  • 每視窗的「首幀已呈現」訊號:沒有一個回呼或事件能告知上層「這個視窗的第一幀已渲染完畢」

  • 建立時的初始背景色:視窗在內容就緒前沒有正確的背景顏色,導致顯示黑色或透明狀態

所以這就導致了 macOS 上視窗時序沒辦法嚴格遵循,而實際上這個問題在單視窗場景下就一直有,很久之前 #55427(2020 年提的) 就有類似:

Consider hiding windows until the engine is active in the desktop runner templates

這上面的 macOS 問題一直都還沒被修復,因為系統機制造成,Windows 的修復思路很直接,在 runner 模板裡建立視窗時加 WS_VISIBLE 為 false,然後等 engine 的 first-frame callback 觸發後再 ShowWindow,因為 Win32 API 設計上支援這種模式,callback 機制也在 engine 裡已經存在。

而 macOS 就比較麻煩了,AppKit 的 orderFront:makeKeyAndOrderFront: 是立即生效的,沒有 deferred 參數。

也就是根據需求,需要在 embedder 層向 engine 註冊 per-view 的 first-frame 回呼,而這個機制在 macOS embedder 沒有可用實作,目前 macOS embedder 的 first-frame 通知是 engine 級別的,不是 view 級別的,多視窗下無法精確知道「哪個 view 的第一幀已就緒」。

當然,在 Tooltip/Popup 場景下會好一點,因為用了 alphaValue = 0.0 + positioner 回呼來延遲顯示,但是也是治標不治本,這也是 macOS 目前最大痛點。

Linux

而 Linux 就更糟心了,相信用 Linux 的應該都懂,這就不是人力問題,而是 GTK3 的 OpenGL 上下文架構從設計上就不支援多個 GtkGLArea 共享同一個 GL 上下文,這導致多 view 渲染在 Linux 上面臨需要繞過 GTK 底層限制的工程問題,感受一下:

時間事件2023 年 11 月issue #138178 由 dkwingsmt 建立,標註 "mostly for tracking purposes",無人認領2024 年 7 月engine PR #54018 作為第一步合入(僅是基礎結構調整)2024 年 10 月prototype 可用,正在拆分為可提交的 PR;同時揭示了核心技術問題(GTK3 GL 上下文限制)2024 年 10 月engine PR #55541(view ID 分配)、#55542(view ID 釋放)合入,這是僅有的兩個實質進展2025 年 6 月robert-ancell 提交 draft PR #170045,嘗試用 EGL 繞過 GTK3 限制,但標註 "not working on X11"2025 年 7 月PR #170045 被關閉,未合入,X11 fallback 未完成2026 年 1 月bot 自動提醒 assignee 沒有進展,robert-ancell 回覆 "Still chipping away at this"(仍在做)2026 年 3 月issue #183911 建立,指出 Linux embedder 多 view 下 shader 需要共享,是又一個新的 P2 子問題2026 年 5 月bot 再次提醒無進展,assignee 被系統自動移除,issue 目前無人認領GTK3 的 GtkGLArea 每個實例都有獨立的 GdkGLContext,這些上下文之間預設不能共享 texture、framebuffer 等 GL 資源。

在單視窗場景下,Flutter engine 把畫面渲染到一個 GdkGLContext 裡就結束了,但是啊,多視窗下 engine 需要把不同 view 的畫面分別 present 到對應的 GtkGLArea,而各自的 GL 上下文是隔離的,engine 沒辦法直接把一個 view 的渲染結果跨上下文傳遞給另一個視窗。

而 prototype 採用的繞過方案,在 implicit view(第一個視窗)的 GL 上下文裡渲染所有內容,然後透過 CPU 回讀再寫入其他視窗的上下文,但是這一聽就知道多不靠譜,CPU readback 本身就是 GPU pipeline 的效能殺手。

另外還有提到用獨立的 EGL 上下文取代 GdkGLContextEGLImage 是一種可以在 EGL 上下文之間共享 texture 的機制,不需要 CPU 拷貝。

但是,但是,但是這個方案在 X11 上不工作啊 ,X11 和 Wayland 在 EGL 的實作細節上有差異,EGLImage 在 X11 上的驅動支援沒有在 Wayland 上那麼普遍,而且 X11 的 GdkGLContext 是基於 GLX 而不是 EGL 的,和 EGL-based 的 Flutter context 之間需要額外的 interop,這部分還沒有實作,所以路子又窄了。

這也是 Linux 的另一個獨特麻煩,需要同時支援 X11 和 Wayland 兩套顯示協定,而這兩者在底層 GL/EGL 棧上的行為有明顯差異:

  • Wayland 上 EGL 是原生的,EGLImage 擴充支援普遍,EGL context 和 GTK4/Wayland surface 的整合有官方文件
  • X11 上 GTK 傳統上使用 GLX(不是 EGL),與 EGL-based 的 Flutter rendering context 之間需要額外的互操作層,沒有乾淨的官方路徑
  • 即便強制在 X11 上用 EGL(透過 EGL_EXT_platform_x11),擴充的驅動支援也不如 Wayland 普遍,開源驅動(Mesa)和私有驅動(NVIDIA)行為不一致

這也是為什麼很多對支援 Linux 不熱衷的原因。

最後

不管怎麼說,多視窗確實推進得有些久了,Canonical 的投入也一直不溫不火,感覺 AI 時代了,再不加速推進 release,感覺多視窗就要爛在手裡了,不過目前至少我在 Win11 場景上還行。


原文出處:https://juejin.cn/post/7644026750097883179


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝12   💬4   ❤️1
455
🥈
alicec
📝1   ❤️2
86
#4
我愛JS
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登