---
title: "地獄難度：手機相機效能優化"
description: "相機是手機上最難搞的一塊：每秒幾十張畫面，每一張都在跟你的 CPU、GPU、記憶體搶資源。"
author: "kidneyweakx"
published: 2026-06-02T17:45:09.347Z
updated: 2026-10-29T10:17:47.594Z
lang: zh-TW
tags: ["react-native", "vision-camera", "ios", "效能"]
canonical: https://kidneyweakx.com/blog/%E5%9C%B0%E7%8D%84%E9%9B%A3%E5%BA%A6-%E6%89%8B%E6%A9%9F%E7%9B%B8%E6%A9%9F%E6%95%88%E8%83%BD%E5%84%AA%E5%8C%96
---

# 地獄難度：手機相機效能優化

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

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

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

<br />

## 第一層地獄：每一幀都很貴

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」，只能自己一層一層挖。

<br />

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

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

```
abort() called
```

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

<br />

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

**兇手一：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 之間都安全的通道。

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

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

<br />

## 學到的事

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

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