最近 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。 WaitForFence()。 vkWaitForFences(..., timeout=UINT64_MAX) 就會無限等待一個永遠不會被 signal 的 fence。 這也是為什麼影片播放時更容易觸發:影片播放幀率高、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;
}
修復邏輯重點:
Present 真正成功(表示該幀已被提交給 GPU 呈現、fence 應該會被 signal)後,才把 fence 標記為 pending(acquire_fence_pending = true)。 vkAcquireNextImageKHR 失敗,該幀根本沒有 Present,fence 不會是 pending,下一次就直接跳過等待。那為什麼一定要升級?因為這個修復只被 backport 到 3.41.6;除非你自己編譯本地 Engine,否則只能透過升級版本來拿到修復。
不過這個改動其實不大,如果你習慣自己編譯 Engine,也不是做不到。
如果短期內不能升級,可以採取一些迴避方式降低風險:
io.flutter.embedding.android.EnableImpeller=false,這可以暫時解決,因為問題主要來自 Impeller 的 Vulkan 路徑。 核心思路是:儘量避免那些會導致 BufferQueue/Surface 消費端暫停的情境,因為任何一次 vkAcquireNextImageKHR 失敗,都可能是觸發點。常見觸發場景包括:
因此如果不想升級,就必須盡量避免上述場景;不過最保險的做法仍然是升級到 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 過
場景 B:App 已運行足夠久,所有 3 個 slot 都被正常 Present 過多次
真正會觸發死鎖的情況(場景 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
vkAcquireNextImageKHRwas returningVK_ERROR_SURFACE_LOST_KHRbecause it was unable to dequeue a buffer. Android logged it asdequeueBuffer failed: Try again (-11)"
我的猜測是,在 Android 16 March Update 之前,SCREEN_OFF 時 Android 的 BufferQueue 有一個「寬限期」:
舊行為(Android < 16 之前):
vkAcquireNextImageKHR 在短時間內仍能成功 acquire(buffer 還可用)而在 Android 16 March Update 之後,流程可能變成:
新行為(Android 16 March Update 之後):
vkAcquireNextImageKHR 幾乎立刻回傳 SURFACE_LOST(沒有寬限期)總結一下原因與影響:
vkAcquireNextImageKHR 開始立刻失敗,導致在新版系統上 bug 必現。因此,最直接的解法是升級到 Flutter 3.41.6;如果無法立刻升級,則採用上述迴避策略(關閉 Impeller、在鎖屏前停止影片播放、降低後台時的 UI/BufferQueue 使用等)來降低風險。總之,建議盡快升級到 3.41.6。