# ITMS-90338 私有 API 报错：Expo 修复

> ITMS-90338：你的构建引用了 Apple 认定为私有的方法、函数或类。找到出问题的依赖，更新它，然后重新构建。

原始页面：[ITMS-90338 私有 API 报错：Expo 修复](https://devoria.fr/zh-hans/blog/itms-90338-non-public-api-usage-expo)
作者：[Mehdi Azizi](https://devoria.fr/zh-hans/mehdi-azizi)
发布：2026-11-04
内容最后修订：2026-11-04


ITMS-90338 是 Apple 对你的构建做的一项自动检查。它的意思是：你的二进制文件引用了 Apple 视为私有的 API，包括 Objective-C 方法、函数或类。**修复方法：找到邮件里提到的文件，顺藤摸瓜找到包含该名称的依赖，更新它或者给你自己的方法改名，然后重新构建。**

用 Expo 的话，这个名称几乎总是来自某个原生库，很少来自你的 JavaScript。2026 年 10 月 15 日，我在 Solys（我做的小游戏闹钟，Expo SDK 57）的模拟器构建上，以及 Xcode 26.2 的 iOS SDK 上，跑了本文所有的分析命令。下面讲怎么读这封邮件、怎么揪出罪魁祸首，以及怎么识别误报。

## 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 月的一封邮件还建议用 `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-hans/blog/guideline-2-5-1-public-api-sdk-expo) 那篇指南里有详细讲解。本文只处理上传时的报错。

和其他 ITMS 错误一样，没有人打开过你的 app：是 Apple 在处理构建时分析了二进制文件。**Apple 没有公布任何私有 API 清单：你邮件里的那些名称，就是唯一可靠的清单。**

## 读路径：是某个 SDK，还是你的可执行文件？

列表前面写的路径告诉你该去哪个文件里找。

- **`Frameworks/Nom.framework/Nom`**：名称位于一个内嵌的动态框架里，也就是某个具体的 SDK。在 Solys 的构建中，这个目录有 14 个框架：Hermes、React Native、几个以预编译形式提供的 Expo 模块，以及图片库 SDWebImage。
- **只有你的 app 名称**：名称位于主可执行文件里。在 Expo 中，它包含你的原生代码和从源码编译的 pod，默认静态链接。这时任何一个原生库都可能是元凶。

## 决策树：从报出的名称到修复

按顺序看下面每一行。第一条符合你情况的，就是线索。

| 你看到的                   | 去哪里找               | 修复方法         |
| -------------------------- | ---------------------- | ---------------- |
| 以 `$s` 或 `_$s` 开头      | `xcrun swift-demangle` | 还原后重走决策树 |
| 在你的 IPA 里找不到        | 对 bundle 执行 `grep`  | 重新提交构建     |
| 在 iOS 上是公开名称        | iOS SDK 头文件         | 重新提交构建     |
| `API_UNAVAILABLE(ios)`     | iOS SDK 头文件         | 更新该包         |
| 在 `Frameworks/X` 里       | 提供 X 的 SDK          | 更新该 SDK       |
| 选择器，在你的 app 里      | pod 和原生模块         | 加前缀或更新     |
| 符号 `_nom`，在你的 app 里 | 先 `nm -m`，再查 pod   | 替换该调用       |
| 类，在你的 app 里          | 你的类和你的 pod       | 给类加前缀       |

写着「重新提交构建」的行代表误报，后文会讲。

## 第 1 步：打开已上传的构建

要分析的是真正发给 Apple 的那个文件，而不是你的项目。从 expo.dev 上的构建页面下载 IPA，然后在 Mac 上运行：

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

第二行列出内嵌的框架。第三行检查是否直接链接了 Apple 的私有框架：它应该什么都不输出。在 Solys 上，`otool -L` 无论在可执行文件里还是在那 14 个框架里，都没有找到任何私有框架。

## 第 2 步：找到包含该名称的文件

先大范围搜。在整个 bundle 里搜索报出的名称，去掉开头的下划线：

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

这条命令会列出包含该文本的文件。然后根据名称类型（选择器、符号、类），在找到的文件上精确查找，下面用 `Binary` 表示这个文件：

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

**查选择器要用 `strings`，不要用 `nm`。** 在 Solys 的 React 框架上，`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 邮件的名称搜索，结果如下：

| 报出的名称                    | 在 iOS SDK 中        | 解读       |
| ----------------------------- | -------------------- | ---------- |
| \_dyld_get_shared_cache_range | 不存在               | 私有 API   |
| boldFontOfSize:               | 不存在               | 私有或重名 |
| SCDynamicStoreCreate          | API_UNAVAILABLE(ios) | 仅限 macOS |
| isMainFrame                   | WebKit 的属性        | 公开       |

**名称存在且在 iOS 上可用，说明多半是误报。名称不存在，说明是私有 API 或重名。** 有两个局限：仅限 Swift 的公开 API 不会出现在这些头文件里；而像 `setDelegate:` 这样的访问器，要搜的是属性 `delegate`。

## 第 4 步：追溯到具体的包

如果路径指向某个框架，就用框架名找到 pod，再找到 npm 包：

```bash
grep -n "FrameworkName" ios/Podfile.lock
npm explain package-name
```

如果这个包你从没自己装过，`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 个以 `.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 条规则和许可协议，而且这时承担风险的是你的账号。

然后，用新的[构建号](https://devoria.fr/zh-hans/blog/app-store-connect-build-number-version)重新构建。远程更新解决不了问题：名称在编译后的原生代码里。上传之前，在新的 IPA 上重新执行第 2 步。名称不应该再出现。

## 遇到误报怎么办

Apple 的检查有时会出错。2022 年 6 月 6 日，一批 React Native 和 Flutter app 因为 `setNavigationDelegate:`、`isMainFrame` 这类 WebKit 公开名称收到了 ITMS-90338，其中包括几乎没改动的更新。第二天，一位 Apple 工程师回复说验证端的问题已解决，请大家重新提交构建。这个讨论串保存在 [Expo 的 GitHub 仓库](https://github.com/expo/expo/issues/17768)里。

以下三个迹象同时出现时，多半是误报：

- 这次构建和上一次被接受的构建很相似；
- 第 2 步找不到这个名称，或第 3 步显示它在 iOS 上是公开的；
- 同一天有其他开发者在 Apple 论坛上报告了同样的错误。

这种情况下，先看看 [Apple 系统状态](https://developer.apple.com/system-status/)，然后用新的构建号重新提交。如果错误依旧，把完整的邮件发给 [Apple 开发者支持](https://developer.apple.com/contact/)。至于名称冲突，在 Apple 看来不算误报：改名就是了。

## 下次上传之前

其他 ITMS 错误也有对应的指南：Xcode 版本问题看 [ITMS-90725](https://devoria.fr/zh-hans/blog/itms-90725-sdk-version-issue-expo)，隐私清单看 [ITMS-91053](https://devoria.fr/zh-hans/blog/itms-91053-missing-api-declaration-expo)，签名问题看 [ITMS-90034](https://devoria.fr/zh-hans/blog/itms-90034-missing-or-invalid-signature-fix)。构建被接受之后，在送审之前先用 [TestFlight](https://devoria.fr/zh-hans/blog/testflight-guide-test-iphone-app) 装上试试。

审核员的拒绝需要更冷静地应对：处理流程在[被 Apple 拒绝的 app](https://devoria.fr/zh-hans/expertise/app-rejected-by-apple) 页面，最常见的拒绝原因在 [Apple 为什么拒绝 app](https://devoria.fr/zh-hans/blog/apple-app-rejection-reasons-real-cases) 一文里。

想在上传前检查你的材料，[Review Kit](https://devoria.fr/zh-hans/kits/app-review) 包含我的检查清单，以及会审阅你项目的 Claude Code skills。如果上传卡住了、想找人帮你看看，[和我预约一小时](https://devoria.fr/zh-hans/coaching)：我们视频连线，在你的屏幕上一起过你的项目。如果你正在准备第一次上架，[做一次免费诊断](https://devoria.fr/zh-hans/diagnostic)：它会帮你理清步骤顺序，并把我的 Apple 防拒审检查清单发给你。

## 常见问题

### ITMS-90338 算是 App Review 拒绝吗？

不算。这是构建处理过程中的自动检查，发生在任何人工审核之前。被拒的构建既进不了 TestFlight，也进不了审核。如果是审核员后来发现了私有 API，那属于 guideline 2.5.1 的范畴。

### 能用远程更新修复 ITMS-90338 吗？

不能。报出的名称在二进制文件编译后的原生代码里。远程更新只能改 JavaScript 和资源文件。修好依赖，重新构建，再重新提交。

### 为什么 Apple 报出的方法我从来没写过？

因为它们来自某个内嵌的库，或者是名称冲突：某个依赖声明了一个和私有 API 同名的方法。Apple 读的是二进制里的名称，不是你的源码。用 `grep` 和 `npm explain` 追溯到具体的包。

### 怎么知道某个 API 在 iOS 上是否公开？

在 iOS SDK 的头文件里搜它的名称（路径可以用 `xcrun --sdk iphoneos --show-sdk-path` 获取），然后看它的可用性。Apple 的文档页面也会标明。只有 macOS 文档的函数，在 iPhone 上不算公开 API。

### 修复后需要改构建号吗？

需要。让 EAS 自动递增：同一版本下已经上传过的构建号可能会被以 ITMS-90189 拒绝。


## 你的项目

[Review Kit](https://devoria.fr/zh-hans/kits/app-review)：我的 App Store 审核清单、我被 Apple 拒审后的修正方案、Google Play 审核清单，以及三个 Claude Code skill。¥369。

[iOS & Android Kit](https://devoria.fr/zh-hans/kits/ios)：用 Claude Code 打造你的 iPhone 和 Android 应用：我自己应用的起步代码、上架商店的完整流程，以及全部工具包。¥739。

[诊断：你离自己的 iPhone 应用还差什么](https://devoria.fr/zh-hans/diagnostic)：7 个问题，60 秒：看清真正阻碍你发布 iPhone 应用的原因，拿到你的 4 步路线和适合你的套件。
