# ITMS-90338 修正：Expo 的「Non-public API usage」

> ITMS-90338：你的 build 引用了 Apple 視為私有的方法、函式或類別。找出相關依賴，更新後重新 build。

原始頁面：[ITMS-90338 修正：Expo 的「Non-public API usage」](https://devoria.fr/zh-hant/blog/itms-90338-non-public-api-usage-expo)
作者：[Mehdi Azizi](https://devoria.fr/zh-hant/mehdi-azizi)
發布日期：2026-11-04
內容最後修訂：2026-11-04


ITMS-90338 是 Apple 對你的 build 做的自動檢查。它表示你的二進位檔引用了 Apple 視為私有的 API：Objective-C 方法、函式或類別。**修正方法：找出 e-mail 裡提到的檔案，追到含有該名稱的依賴，更新它或重新命名你的方法，然後重新 build。**

用 Expo 的話，這個名稱幾乎都來自某個原生函式庫，很少來自你的 JavaScript。2026 年 10 月 15 日，我在 Solys（我的小遊戲鬧鐘，Expo SDK 57）的模擬器 build 上，以及 Xcode 26.2 的 iOS SDK 上，跑過這篇指南裡的分析指令。下面告訴你怎麼讀 e-mail、找出元凶，以及辨認誤判。

## ITMS-90338 錯誤是什麼意思？

訊息標題是「ITMS-90338: Non-public API usage」。依 Apple 在你的二進位檔裡找到的東西，有三種版本：

- **「references non-public selectors」**：Objective-C 方法名稱，例如 `boldFontOfSize:`；
- **「references non-public symbols」**：函式，前面帶底線，例如 `__dyld_get_shared_cache_range`；
- **「contains or inherits from non-public classes」**：類別名稱。

每種版本都會給一個路徑，接著是名稱清單。內文還附上兩個建議：如果你的方法名稱和私有 API 相同就重新命名，以及移除含有它們的靜態函式庫。2026 年 9 月的一封 e-mail 也建議用 `xcrun swift-demangle` 把編碼過的 Swift 名稱轉成看得懂的形式。

這項檢查背後的規則是 [guideline 2.5.1](https://developer.apple.com/app-store/review/guidelines/#2.5.1)：app 只能使用公開 API。[Apple Developer Program 授權合約](https://developer.apple.com/support/terms/apple-developer-program-license-agreement/)第 3.3.1 條也重申：不得呼叫私有 API。這條規則的人工審查，在 [guideline 2.5.1](https://devoria.fr/zh-hant/blog/guideline-2-5-1-public-api-sdk-expo) 指南裡有詳細說明。這裡處理的是上傳錯誤。

和其他 ITMS 錯誤一樣，沒有人打開過你的 app：Apple 是在處理 build 時分析二進位檔。**Apple 沒有公布任何私有 API 清單：你 e-mail 裡的名稱就是唯一可靠的清單。**

## 讀懂路徑：是 SDK 還是你的執行檔？

清單前面寫的路徑會告訴你該去哪個檔案找。

- **`Frameworks/Nom.framework/Nom`**：名稱在某個內嵌的動態 framework 裡，也就是某個特定 SDK。在 Solys 的 build 中，這個資料夾有 14 個 framework：Hermes、React Native、已預先編譯的 Expo 模組，以及圖片函式庫 SDWebImage。
- **只有你的 app 名稱**：名稱在主執行檔裡。用 Expo 的話，主執行檔包含你的原生程式碼和從原始碼編譯的 pod，預設是靜態連結。所以任何原生函式庫都可能是元凶。

## 決策樹：從被點名的名稱到修正

依序看每一列。第一個符合你情況的列就是線索。

| 你看到的                   | 去哪裡找               | 修正方式         |
| -------------------------- | ---------------------- | ---------------- |
| `$s` 或 `_$s` 開頭的名稱   | `xcrun swift-demangle` | 重新走一次決策樹 |
| 在你的 IPA 裡找不到        | 對 bundle 執行 `grep`  | 重新上傳 build   |
| iOS 上的公開名稱           | iOS SDK 標頭檔         | 重新上傳 build   |
| `API_UNAVAILABLE(ios)`     | iOS SDK 標頭檔         | 更新套件         |
| 在 `Frameworks/X` 裡       | 提供 X 的 SDK          | 更新 SDK         |
| 選擇器，在你的 app 裡      | Pod 和原生模組         | 加前綴或更新     |
| 符號 `_nom`，在你的 app 裡 | `nm -m`，再查 pod      | 替換呼叫         |
| 類別，在你的 app 裡        | 你的類別和 pod         | 為類別加前綴     |

「重新上傳 build」那幾列代表誤判，下文會說明。

## 步驟 1：打開已上傳的 build

針對實際上傳給 Apple 的檔案操作，而不是你的專案。從 expo.dev 的 build 頁面下載 IPA，然後在你的 Mac 上執行：

```bash
unzip -q MonApp.ipa -d ipa
ls ipa/Payload/MonApp.app/Frameworks
otool -L ipa/Payload/MonApp.app/MonApp | grep PrivateFrameworks
```

第二行列出內嵌的 framework。第三行尋找直接連結到 Apple 私有 framework 的地方：它應該什麼都不顯示。在 Solys 上，`otool -L` 沒找到任何私有 framework，執行檔和 14 個 framework 裡都沒有。

## 步驟 2：找出含有該名稱的檔案

先大範圍搜尋。在整個 bundle 裡找被點名的名稱，拿掉開頭的底線：

```bash
grep -rl "dyld_get_shared_cache_range" ipa/Payload/MonApp.app
```

這個指令會列出含有這段文字的檔案。接著依名稱類型（選擇器、符號、類別），在找到的檔案（記為 `Binaire`）上縮小範圍：

```bash
strings -a Binaire | grep -x "boldFontOfSize:"
nm -m Binaire | grep "_dyld_get_shared_cache_range"
nm -m Binaire | grep '_OBJC_CLASS_$_NomDeClasse'
```

**選擇器要用 `strings`，不要用 `nm`。** 在 Solys 的 React framework 上，`nm` 顯示的是帶類別的方法，例如 `-[RCTRootView initWithBridge:moduleName:initialProperties:]`，從來不會單獨顯示選擇器 `initWithBridge:`。`strings` 找得到。

符號的話，看那一行的結尾。在 Solys 上，`nm -m` 例如會顯示 `(undefined) external __dyld_get_image_name (from libSystem)`：二進位檔呼叫這個函式但不包含它，由 libSystem 提供。這個函式是公開的，下一步會說明。

## 步驟 3：確認名稱是否真的是私有的

iOS SDK 的標頭檔描述了公開 API。花幾秒搜尋，就知道被點名的名稱有沒有在裡面，以及支援哪些平台。符號的話，先拿掉編譯時加上的底線：`_SCDynamicStoreCreate` 變成 `SCDynamicStoreCreate`。

```bash
SDK=$(xcrun --sdk iphoneos --show-sdk-path)
grep -rn "SCDynamicStoreCreate" "$SDK/usr/include" "$SDK"/System/Library/Frameworks/*/Headers
```

2026 年 10 月 15 日，用 iOS 26.2 SDK，取自真實 ITMS-90338 e-mail 的四個名稱結果如下：

| 被點名的名稱                  | 在 iOS SDK 中        | 解讀           |
| ----------------------------- | -------------------- | -------------- |
| \_dyld_get_shared_cache_range | 不存在               | 私有 API       |
| boldFontOfSize:               | 不存在               | 私有或名稱衝突 |
| SCDynamicStoreCreate          | API_UNAVAILABLE(ios) | 僅限 macOS     |
| isMainFrame                   | WebKit 的屬性        | 公開           |

**名稱存在且在 iOS 上可用，代表是誤判。名稱不存在，代表是私有 API 或名稱衝突。** 有兩個限制：只在 Swift 中公開的 API 不會出現在這些標頭檔裡；像 `setDelegate:` 這樣的存取方法，要搜尋屬性 `delegate`。

## 步驟 4：追到套件

如果路徑點名的是 framework，從它的名稱可以找到 pod，再找到 npm 套件：

```bash
grep -n "NomDuFramework" ios/Podfile.lock
npm explain nom-du-paquet
```

如果你從沒自己加過這個套件，`npm explain` 會告訴你是哪個依賴安裝了它。

如果路徑點名的是執行檔，就在原生原始碼裡找這個名稱，再到 pod 裡找：

```bash
grep -rl --include='*.m' --include='*.mm' --include='*.swift' --include='*.h' \
  "boldFontOfSize" node_modules modules
grep -rl "boldFontOfSize" ios/Pods
```

依情況調整資料夾清單：`modules` 是本地 Expo 模組的資料夾。`ios/Pods` 資料夾會在執行 `npx expo prebuild -p ios` 後出現，這個指令在 Mac 上也會安裝 pod。注意：它可能會重建 `ios` 資料夾。如果你手動改過它，請在副本上操作。

**在 `ios/Pods` 裡搜尋，和搜尋原始碼一樣重要。** 在 Solys 上，這個資料夾有 15 個已預先編譯的 framework，格式為 `.xcframework`：Expo 模組、Hermes、React Native 和第三方 SDK。只搜尋原始碼檔案的話，會漏掉它們的程式碼。

## 常見原因

**第三方 SDK 版本太舊或太新。** 2026 年 9 月，一位 Flutter 開發者[回報因某個安全 SDK 被拒](https://github.com/talsec/Free-RASP-Flutter/issues/236)：它的 8.2.3 版呼叫了 `__dyld_get_shared_cache_range`。8.2.2 版可以通過，開發商建議先退回該版本，等待修正。

**呼叫未公開 API 的原生模組。** 你自己的模組、某個外掛或小型函式庫，可能用了在論壇上找到的 UIKit 內部方法。這項檢查會靠名稱抓到它。

**名稱衝突。** 你的方法或類別碰巧和 Apple 的私有 API 同名。訊息已經預料到這種情況，要你重新命名。Apple 建議[為加到它的類別上的方法加前綴](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html)，例如三個小寫字母加一個底線。

**在 macOS 上公開、在 iOS 上不公開的 API。** 2024 年，一個 .NET MAUI app [因 `_SCDynamicStoreCreate` 被拒](https://github.com/dotnet/runtime/issues/103949)。Apple 確實有記載這個函式，但[只限 macOS](https://developer.apple.com/documentation/systemconfiguration/1437828-scdynamicstorecreate)，而 iOS SDK 的標頭檔把它標為 `API_UNAVAILABLE(ios, tvos, watchos)`。

## 正面修正，不要繞過

1. **更新依賴。** 對於 Expo 有追蹤的套件，根據 [Expo CLI 文件](https://docs.expo.dev/more/expo-cli/)，`npx expo install --check` 會指出和你的 SDK 不相符的套件。其他套件就去看版本更新紀錄。
2. **退回上一版**，如果是新版本帶進了這個名稱，並通知開發商。
3. **替換或移除**已停止維護的函式庫。
4. **重新命名**你的方法或類別，加上前綴。

**不要隱藏名稱。** 把字串拆開或用迂迴方式呼叫方法，只是把使用藏起來，沒有修正。這仍然違反 2.5.1 規則和合約，到時候承擔風險的是你的帳號。

接著，用新的 [build 編號](https://devoria.fr/zh-hant/blog/app-store-connect-build-number-version)重新 build。遠端更新不夠：名稱在編譯過的原生程式碼裡。上傳前，在新的 IPA 上再跑一次步驟 2。名稱應該不會再出現。

## 遇到誤判時

Apple 的檢查有時會出錯。2022 年 6 月 6 日，一些 React Native 和 Flutter app 因為 WebKit 的公開名稱（例如 `setNavigationDelegate:` 或 `isMainFrame`）收到 ITMS-90338，連幾乎沒改動的更新也是。隔天，一位 Apple 工程師表示問題已在驗證端解決，並請大家重新上傳 build。這個討論串保存在 [Expo 的 GitHub 儲存庫](https://github.com/expo/expo/issues/17768)。

三個跡象同時出現，代表是誤判：

- 這個 build 和上一個被接受的 build 很相似；
- 步驟 2 找不到這個名稱，或步驟 3 顯示它在 iOS 上是公開的；
- 同一天有其他開發者在 Apple 論壇上回報相同錯誤。

這種情況下，先看 [Apple 服務狀態](https://developer.apple.com/system-status/)，再用新編號重新上傳 build。如果錯誤持續，附上完整 e-mail 寫信給 [Apple 開發者支援](https://developer.apple.com/contact/)。至於名稱衝突，在 Apple 眼中不算誤判：請重新命名。

## 下次上傳之前

其他 ITMS 錯誤也有各自的指南：[ITMS-90725](https://devoria.fr/zh-hant/blog/itms-90725-sdk-version-issue-expo) 是 Xcode 版本，[ITMS-91053](https://devoria.fr/zh-hant/blog/itms-91053-missing-api-declaration-expo) 是隱私權清單，[ITMS-90034](https://devoria.fr/zh-hant/blog/itms-90034-missing-invalid-signature-fix) 是簽章。build 被接受後，在審查前先用 [TestFlight](https://devoria.fr/zh-hant/blog/testflight-test-iphone-app-before-review) 安裝它。

審查人員的拒絕需要更多思考：處理流程在 [app 被 Apple 拒絕](https://devoria.fr/zh-hant/expertise/app-rejected-by-apple)頁面，最常見的原因在[Apple 為什麼拒絕 app](https://devoria.fr/zh-hant/blog/apple-app-rejection-reasons-real-cases)。

想在上傳前檢查你的資料，[Review Kit](https://devoria.fr/zh-hant/kits/app-review) 整合了我的檢查清單和會檢閱你專案的 Claude Code skills。如果上傳卡住、想找個人幫你看看，[跟我預約一小時](https://devoria.fr/zh-hant/coaching)：我們透過視訊，在你的螢幕上一起檢查你的專案。如果你正在準備第一次上架，[做個免費診斷](https://devoria.fr/zh-hant/diagnostic)：它會幫你把步驟排好順序，並寄給你我的 Apple 防退件檢查清單。

## 常見問題

### ITMS-90338 是 App Review 的拒絕嗎？

不是。這是處理 build 時的自動檢查，在任何人工審查之前。被拒的 build 不會進入 TestFlight，也不會進入審查。之後審查人員發現的私有 API，則屬於 guideline 2.5.1 的範圍。

### 可以用遠端更新修正 ITMS-90338 嗎？

不行。被點名的名稱在二進位檔編譯過的原生程式碼裡。遠端更新只會改變 JavaScript 和資源。修正依賴，重新 build，再重新上傳。

### 為什麼 Apple 點名我從沒寫過的方法？

因為它們來自內嵌的函式庫，或是名稱衝突：某個依賴宣告了一個和私有 API 同名的方法。Apple 讀的是二進位檔裡的名稱，不是你的原始碼。用 `grep` 和 `npm explain` 追到套件。

### 怎麼知道某個 API 在 iOS 上是不是公開的？

在 iOS SDK 的標頭檔裡搜尋它的名稱（路徑可用 `xcrun --sdk iphoneos --show-sdk-path` 取得），再看它的可用性。Apple 的文件頁面也有標示。只記載於 macOS 的函式，在 iPhone 上不算公開。

### 修正後需要更改 build 編號嗎？

需要。讓 EAS 自動遞增：同一版本已上傳過的編號，可能會被 ITMS-90189 拒絕。


## 你的專案

[Review Kit](https://devoria.fr/zh-hant/kits/app-review)：我的 App Store 審查檢查清單、我被 Apple 退件後的修正、Google Play 檢查清單，以及三個 Claude Code skill。$1,749。

[iOS & Android Kit](https://devoria.fr/zh-hant/kits/ios)：用 Claude Code 打造你的 iPhone 和 Android App：我自己 App 的起始程式碼、上架商店的完整流程，以及所有套件。$3,599。

[診斷：你離自己的 iPhone App 還差什麼](https://devoria.fr/zh-hant/diagnostic)：7 個問題，60 秒：找出真正卡住你發布 iPhone App 的原因，拿到你的 4 步驟路線，以及最適合你的套件。
