QUEST LOG · June 2, 2026· UPDATED October 29, 2026

地獄難度:手機相機效能優化

相機是手機上最難搞的一塊:每秒幾十張畫面,每一張都在跟你的 CPU、GPU、記憶體搶資源。

這一年做了兩個重度用相機的 app:Solid(ar)ity 要掃 QR、要讀護照上的 MRZ(就是護照最下面那兩行看不懂的字),另一個巡禮 app 要在相機上疊動畫截圖、做即時對位。兩個都是 React Native + VisionCamera。

做完的心得只有一句:相機這條路,是地獄難度。


# 第一層地獄:每一幀 (Frame) 都很貴

VisionCamera 的 frame processor 讓你可以在每一幀上跑程式。聽起來很美好,但算一下:30fps 就是每 33 毫秒要處理完一張全解析度的畫面,處理不完就掉幀、發燙、耗電。

解法:先縮小再說。

大部分的分析根本不需要全解析度。巡禮 app 要算畫面的亮度跟色溫,我們直接用 GPU resizer 把畫面降到 64×64 再算平均值。對 OCR 這種需要細節的,也只切 ROI(感興趣的區域)出來,不要整張丟進去。

能在 GPU 做的,就不要搬回 CPU。 這一條省下的效能比任何其他做的亂七八糟的優化都多。

# 第二層地獄:Xcode Cloud 不幫你編 shader

縮圖用的 VisionCameraResizer 是 Metal shader。本機跑得好好的,丟上 Xcode Cloud 打包就掛了,因為 CI 環境沒有 Metal toolchain。

最後的解法是把 shader 預先編譯成 metallib,跟著專案一起進版控,CI 就不用現場編譯。這種問題沒有任何錯誤訊息會告訴你「去預編譯 shader」,只能自己一層一層挖。但是問題就是整體本地編譯時間變更長了...


# 第三層地獄:閃退,而且不告訴你為什麼

這是真正的地獄。Solid(ar)ity 的護照 MRZ 掃描,在一台 iPhone XR(A12)上會隨機閃退。crash report 裡只有一行:

code
abort() called

沒有例外類型,沒有訊息,沒有 JS stack。什麼都沒有。


追了很久才挖出兩個兇手:

兇手一:worklet 裡的 console.log

OCR 的 worklet 跑在 VisionCamera 的 async-runner runtime 上,不是 Reanimated 的 UI runtime。這個 runtime 沒有 JS 排程的那些全域函式,所以:

  • console.log(...) 會丟例外(它底層會走 scheduleOnRN)
  • 例外從 runAsync 一路往上冒,穿過 Nitro,直接 std::terminate 整個 app

對,你加一行 log 想 debug,結果 log 本身就是讓 app 閃退的原因。這是我今年遇過最荒謬的 bug。

兇手二:Apple Vision 在 A12 上丟 C++ 例外

Vision 的文字辨識用 .accurate 模式時,在 A12 的神經網路引擎上會在推論中途丟出一個 C++ 例外,不是 NSException。所以 Objective-C 的 @catch (NSException *) 完全接不到,一定要用 catch (...)。

# 怎麼爬出來

1. 讓閃退自己報上名字

我們在 app 啟動時裝了一個全域的 std::set_terminate handler。任何沒被接住的例外,都會把型別跟訊息寫進 crash report 的 Application Specific Information,再打一條 os_log。從此 crash report 不再只有 abort() called,而是直接告訴你是哪個函式炸的。光這一招,就把除錯從幾個小時變成幾分鐘。

2. worklet 要寫成「不可能逃出去」

worklet 的本體全部包在 try/catch/finally 裡,catch 跟 finally 只寫 SharedValue 或直接吞掉,絕對不呼叫任何可能丟例外的東西,連 frame.dispose() 都要保護。

結果要回傳給 JS 的話,就寫進 SharedValue,JS 這邊每 100 毫秒輪詢一次。看起來很土,但它是唯一一條在所有 runtime 之間都安全的通道。

只能說這真是選 React Native 做原生痛苦的點

3. 原生模組不要每一幀都丟例外

Nitro 的方法遇到暫時性的失敗(這一幀沒對焦、沒偵測到文字),就回傳空結果,讓呼叫端跳過這一幀。每一幀都跨邊界丟例外,遲早有一幀會 terminate 你。


# TLDR

  1. 相機效能的第一原則是少做事:降解析度、切 ROI、能在 GPU 做就在 GPU 做
  2. 要知道你的程式碼跑在哪個 runtime:同一個 JS 函式,在 UI runtime 沒事,在 async-runner 會殺掉整個 app
  3. 閃退要能自我介紹:先把診斷工具裝好,再來抓 bug。不然你只能對著 abort() called 通靈
  4. 一定要有一台舊手機:模擬器跟新手機都測不出來,A12 才是照妖鏡,好險開發者夠窮,只能買 iPhoneXR

相機這塊真的是地獄難度,但爬出來之後,對 React Native 底下到底在發生什麼事,理解深了不只一層。

我做的 App

iOS 與 Android 都能免費下載。

AniSeekr

AniSeekr

一站同步 MAL、AniList、Bangumi 等六大平台的追番清單,再帶你去作品裡真實存在的聖地。

Solid(ar)ity

Solid(ar)ity

隱私優先的身分錢包:零知識證明加上面對面交換的簽章名片,沒有中心伺服器。