Flutter 3.41.6 版本很重要,你大概率需要更新一下

最近 Flutter 更新了一個 hotfix 小版本 3.41.6,雖然是小版本,但它解決了一個長期以來的神祕 bug:Android App 在鎖屏或處於後台時,會出現 ANR(應用無回應) 的情況。

這個 bug 能在這次被修復,很大程度要歸功於 Android 16 的 2026 年 3 月安全更新。該更新把這個歷史 bug 的觸發條件放大,所以更容易重現。最容易出問題的場景是:

Android 裝置按下電源鍵(SCREEN_OFF)或 App 切換到後台,同時 Flutter 層正在播放影片

不是說只有播放影片才會觸發,但播放影片會讓這種情況更容易出現。一旦觸發,log 中會看到 ErrorSurfaceLostKHR,App 會出現 ANR 死鎖:畫面點亮後無法渲染新幀,甚至直接崩潰。

問題根本原因在於 Vulkan 的 swapchain 與 fence 機制。目前 Flutter 使用 Impeller 作為渲染引擎,在 Android 上使用 Vulkan 後端。Vulkan 渲染一幀的標準流程大致如下:

vkAcquireNextImageKHR()   ← 從 swapchain 取得下一張可用影像,同時傳入一個 fence
       ↓ (渲染)
vkQueueSubmit()            ← 提交渲染命令
       ↓
vkQueuePresentKHR()        ← 將影像呈現到畫面 (Present)

這裡的 Fence(柵欄)是 GPU 的同步原語,vkAcquireNextImageKHR 會在 GPU 真正完成影像並可用時,去 signal(觸發)這個 fence。

下一次進入同一個 frame slot 時,就必須先呼叫 vkWaitForFences() 等待這個 fence 被 signal,確保上一幀的 acquire 已完成,才能安全重複使用這個 slot。

在這個 bug 中,正是上述流程出現了死鎖:

1) SCREEN_OFF 觸發 Surface 銷毀
Android 系統在螢幕關閉時,會銷毀當前 App 的 ANativeWindow / VkSurfaceKHR(Vulkan Surface)。

2) vkAcquireNextImageKHR 回傳 VK_ERROR_SURFACE_LOST_KHR
當 Impeller 的 swapchain 嘗試取得下一幀影像時,因為 Surface 已被系統銷毀,Vulkan driver 回傳 VK_ERROR_SURFACE_LOST_KHR(對應 Flutter log 的 ErrorSurfaceLostKHR)。此時 AcquireNextDrawable() 回傳 nullptr,Flutter 無法渲染該幀,也不會走到 Present 流程

3) Fence 永遠不會被 signal
在修復之前的程式邏輯(KHRFrameSynchronizerVK)沒有追蹤 fence 是否處於 pending 狀態,因此會導致繪製停滯:

// 修復前:fence 初始化時帶 eSignaled 標誌(已觸發狀態)
auto acquire_res = device.createFenceUnique(
    vk::FenceCreateInfo{vk::FenceCreateFlagBits::eSignaled}); // ← 問題起點

正常流程:

  • vkAcquireNextImageKHR 成功後,fence 會在 GPU acquire 完成後被 signal。
  • 下幀 WaitForFence() 等到 signal,然後重置 fence,再繼續。

但在異常流程(SCREEN_OFF)時:

  • vkAcquireNextImageKHR 失敗 (VK_ERROR_SURFACE_LOST_KHR),而 fence 從未被送到 GPU,因此永遠不會被 signal。
  • 代碼不知道這件事,下次進到同一個 frame slot 時仍然呼叫 WaitForFence()
  • vkWaitForFences(..., timeout=UINT64_MAX) 就會無限等待一個永遠不會被 signal 的 fence
  • 主執行緒(UI Thread)被永久阻塞,造成 ANR

這也是為什麼影片播放時更容易觸發:影片播放幀率高、Vulkan swapchain 呼叫頻繁,Surface 銷毀與 vkAcquireNextImageKHR 的呼叫窗口變小,產生死鎖的條件更容易命中。

至於為什麼 Android 16 的 3 月安全更新特別容易觸發,最有可能是該更新改變了系統銷毀 Surface 的時序(例如更積極/更快),我推測可能是 GPU driver 的更新,導致原本在較寬鬆時序下偶發的競態條件變成必現。

在 PR #183288(針對 khr_swapchain_impl_vk.cc)中的修復如下,核心是加入對 fence 是否 pending 的追蹤:

struct KHRFrameSynchronizerVK {
  vk::UniqueFence acquire;
  bool acquire_fence_pending = false;  // ← 新增:追蹤 fence 是否處於 pending 狀態
  vk::UniqueSemaphore render_ready;
  ...

  explicit KHRFrameSynchronizerVK(const vk::Device& device) {
    // 修復前:初始化時帶 eSignaled(已觸發),導致第一次 WaitForFence 可以通過
    // 修復後:不帶 eSignaled,初始為未觸發狀態
    auto acquire_res = device.createFenceUnique({});
    ...
  }

  bool WaitForFence(const vk::Device& device) {
    // 關鍵修復:如果 fence 從未被 pending(即 acquire 從未成功),直接跳過等待
    if (!acquire_fence_pending) {
      return true;
    }
    if (auto result = device.waitForFences(...); result != vk::Result::eSuccess) {
      return false;
    }
    acquire_fence_pending = false;  // 等待成功後重置標誌
    ...
  }
};

bool KHRSwapchainImplVK::Present(...) {
  ...
  // vkQueuePresentKHR 成功後,標記 fence 為 pending
  sync->acquire_fence_pending = true;
}

修復邏輯重點:

  • Fence 初始不再帶 eSignaled(不預先標記為已觸發)。
  • 只有在 Present 真正成功(表示該幀已被提交給 GPU 呈現、fence 應該會被 signal)後,才把 fence 標記為 pending(acquire_fence_pending = true)。
  • vkAcquireNextImageKHR 失敗,該幀根本沒有 Present,fence 不會是 pending,下一次就直接跳過等待。

那為什麼一定要升級?因為這個修復只被 backport 到 3.41.6;除非你自己編譯本地 Engine,否則只能透過升級版本來拿到修復。

不過這個改動其實不大,如果你習慣自己編譯 Engine,也不是做不到。

如果短期內不能升級,可以採取一些迴避方式降低風險:

  • 關閉 Impeller,退回 Skia:在 AndroidManifest.xml 中設定 io.flutter.embedding.android.EnableImpeller=false,這可以暫時解決,因為問題主要來自 Impeller 的 Vulkan 路徑。
  • 監聽 AppLifecycleState(inactive/paused),在螢幕關閉前主動停止影片播放,避免出現競態窗口。
  • 避免在鎖屏或背景時仍有頻繁的 UI 更新或動畫(任何會觸發 BufferQueue 消費暫停的情況都可能觸發)。

核心思路是:儘量避免那些會導致 BufferQueue/Surface 消費端暫停的情境,因為任何一次 vkAcquireNextImageKHR 失敗,都可能是觸發點。常見觸發場景包括:

  • 按電源鍵關屏(SCREEN_OFF):SurfaceFlinger 停止消費 buffer,隊列被填滿。
  • App 切換到後台:Surface 被系統回收或暫停。
  • 影片/媒體播放時系統 buffer 壓力大,BufferQueue 被媒體佔用,Flutter 搶不到。
  • 螢幕旋轉瞬間舊 Surface 被銷毀、新 Surface 尚未就緒。
  • 多視窗 / 分割畫面切換,Surface resize/recreate 的過渡期。
  • 部分 OEM 的省電策略或後台渲染限制。
  • Android 系統記憶體壓力導致 SurfaceFlinger 主動回收 Surface。

因此如果不想升級,就必須盡量避免上述場景;不過最保險的做法仍然是升級到 3.41.6。

為什麼在 Android 16 更新之前這個問題看起來是「玄學」的?因為有幾層巧合維持了脆弱的平衡,修復前的行為相當依賴這些「僥倖機制」:

修復前的 KHRFrameSynchronizerVK 結構沒有 acquire_fence_pending,且 fence 在建立時帶有 eSignaled

struct KHRFrameSynchronizerVK {
  vk::UniqueFence acquire;
  // ← 注意:沒有 acquire_fence_pending 標誌!

  explicit KHRFrameSynchronizerVK(const vk::Device& device) {
    // ↓↓↓ 關鍵:fence 建立時帶 eSignaled 標誌 ↓↓↓
    auto acquire_res = device.createFenceUnique(
        vk::FenceCreateInfo{vk::FenceCreateFlagBits::eSignaled});
    ...
  }

  bool WaitForFence(const vk::Device& device) {
    // ← 無論如何都直接等,沒有任何保護
    if (auto result = device.waitForFences(
            *acquire,
            true,
            std::numeric_limits<uint64_t>::max()  // ← 無限超時!
        ); result != vk::Result::eSuccess) {
      ...
    }
    ...
  }
};

修復前其實存在兩層「保護」使得問題不易被發現:

第一層保護:eSignaled 初始化
vk::FenceCreateFlagBits::eSignaled 表示 fence 在建立時就處於「已觸發(SIGNALED)」狀態。第一次使用 frame slot 時,不需要等待任何 GPU 操作完成,所以把 fence 預先設為 SIGNALED,讓第一次 WaitForFences 馬上通過,然後再 reset。

Frame slot 的正常生命週期(簡化):

初始:fence = SIGNALED(eSignaled 初始化)
     ↓
第1幀:WaitForFence → 立刻通過(因為 SIGNALED)→ ResetFence → UNSIGNALED
       vkAcquireNextImageKHR(傳入 fence)→ GPU acquire 完成時 fence 變 SIGNALED
       Present 成功
     ↓
第2幀:WaitForFence → 等 GPU → SIGNALED → 通過 → ResetFence ...

第二層保護(僥倖點):kMaxFramesInFlight 的輪替
修復前 kMaxFramesInFlight = 3(現在也是),也就是有 3 個 frame slot 輪流使用:slot 0、1、2、0、1、2… 每個 slot 在建立時都帶 eSignaled,而且只有第一次使用時會「消耗」這個初始 signal。SCREEN_OFF 的場景在修復前如下:

假設 slot 0 正在使用中,SCREEN_OFF 發生:

第N幀:WaitForFence(slot 0) → 通過(上幀 GPU 已完成)
       vkAcquireNextImageKHR(slot 0) → 失敗!SURFACE_LOST
       ← fence 沒有被傳給 GPU,永遠不會 signal!
       Present 未呼叫
            ↓
第N+1幀:換到 slot 1
       WaitForFence(slot 1) → ???(可能是初始 SIGNALED)

關鍵在於:slot 1、slot 2 的 fence 狀態是否還保有初始的 SIGNALED?這取決於 App 運行狀況,這也解釋了為何問題以前是「隨機」或難以復現:

場景 A:App 剛啟動不久,slot 1/2 從未被 Present 過

  • slot 1 的 fence 狀態:eSignaled(初始值,從未被 reset 過)
  • WaitForFence(slot 1) → 立刻通過 → 渲染繼續,沒有死鎖

場景 B:App 已運行足夠久,所有 3 個 slot 都被正常 Present 過多次

  • slot 1 的 fence 狀態:UNSIGNALED(上一次正常 Present 後被 reset)
  • 但上次正常 Present 時 GPU 已 signal,WaitForFence(slot 1) → 等到 signal → 通過 → 繼續,沒有死鎖

真正會觸發死鎖的情況(場景 C):

第N幀:slot X acquire 失敗(SURFACE_LOST)→ fence UNSIGNALED,永不 signal(埋下炸彈)
第N+1幀:換 slot Y → WaitForFence(Y) 通過(Y 上次正常完成)
第N+2幀:換 slot Z → WaitForFence(Z) 通過(Z 上次正常完成)
第N+3幀:回到 slot X → WaitForFence(X) → 等待永遠不會 signal 的 fence → 死鎖!

死鎖要等到繞一圈 3 個 slot 之後,再次回到出問題的那個 slot 才會爆發,這就是為何之前很難發現:問題不是立即爆發,而是延遲 2~3 幀之後才觸發。

時間軸(簡化):

T0: SCREEN_OFF → slot 0 acquire 失敗 → 埋下炸彈
T1: 切換 slot 1 → WaitForFence 正常通過 → 表面看起來正常
T2: 切換 slot 2 → WaitForFence 正常通過 → 表面看起來正常
T3: 回到 slot 0 → WaitForFence 無限等待 → ANR

ANR 預設會等約 5 秒才跳出「無回應」對話框,這段時間 App 在 T0–T2 看起來還活著,使用者或開發者很難把 T0 的 SCREEN_OFF 與 T3 的 ANR 連在一起。

正如修復中提到:

"Android's implementation of vkAcquireNextImageKHR was returning VK_ERROR_SURFACE_LOST_KHR because it was unable to dequeue a buffer. Android logged it as dequeueBuffer failed: Try again (-11)"

我的猜測是,在 Android 16 March Update 之前,SCREEN_OFF 時 Android 的 BufferQueue 有一個「寬限期」:

舊行為(Android < 16 之前):

  • SCREEN_OFF
  • SurfaceFlinger 停止消費,但 BufferQueue 尚保留 1~2 個空槽
  • vkAcquireNextImageKHR 在短時間內仍能成功 acquire(buffer 還可用)
  • Flutter 完成了 Present,GPU signal 了 fence
  • Surface 銷毀時,Flutter 已完整走完一幀,fence 處於 SIGNALED
  • 下一幀 WaitForFence → 正常通過 → 沒有死鎖

而在 Android 16 March Update 之後,流程可能變成:

新行為(Android 16 March Update 之後):

  • SCREEN_OFF
  • Surface/BufferQueue 被更積極地立即回收(GPU driver 或 SurfaceFlinger 行為改變)
  • vkAcquireNextImageKHR 幾乎立刻回傳 SURFACE_LOST(沒有寬限期)
  • fence 從未被 GPU touch → 永久 UNSIGNALED
  • 3 幀後 WaitForFence → 無限等待 → ANR

總結一下原因與影響:

  • eSignaled 初始化:每個 frame slot 的初始 fence 為 SIGNALED,讓第一次 WaitForFence 無條件通過,這在 App 啟動階段能完全免疫問題。
  • 3 個 slot 的輪轉:出問題的 slot 要等繞完一圈才會再次被訪問,其他 slot 正常工作會延遲爆發時機。
  • Android 舊 BufferQueue 的寬限期:SCREEN_OFF 後 acquire 仍能短暫成功,fence 被正常 signal,從根本上消除了觸發條件。
  • Android 16 的更新讓寬限期失效,vkAcquireNextImageKHR 開始立刻失敗,導致在新版系統上 bug 必現。

因此,最直接的解法是升級到 Flutter 3.41.6;如果無法立刻升級,則採用上述迴避策略(關閉 Impeller、在鎖屏前停止影片播放、降低後台時的 UI/BufferQueue 使用等)來降低風險。總之,建議盡快升級到 3.41.6。


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


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

共有 0 則留言


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