ITMS-90338 私有 API 报错:Expo 修复
作者:Mehdi Azizi
发布于 ,9分钟
ITMS-90338:你的构建引用了 Apple 认定为私有的方法、函数或类。找到出问题的依赖,更新它,然后重新构建。
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:app 只能使用公开 API。Apple Developer Program 许可协议第 3.3.1 条也重申了这一点:不得调用任何私有 API。这条规则的人工审核部分,在 guideline 2.5.1 那篇指南里有详细讲解。本文只处理上传时的报错。
和其他 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 上运行:
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 里搜索报出的名称,去掉开头的下划线:
grep -rl "dyld_get_shared_cache_range" ipa/Payload/MyApp.app
这条命令会列出包含该文本的文件。然后根据名称类型(选择器、符号、类),在找到的文件上精确查找,下面用 Binary 表示这个文件:
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。
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 包:
grep -n "FrameworkName" ios/Podfile.lock
npm explain package-name
如果这个包你从没自己装过,npm explain 会告诉你是哪个依赖把它装进来的。
如果路径指向可执行文件,就先在原生源码里搜这个名称,再到 pod 里搜:
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 被拒:它的 8.2.3 版调用了 __dyld_get_shared_cache_range。8.2.2 版能通过,开发商建议在修复发布前先退回这个版本。
某个原生模块调用了未公开的 API。 你自己的模块、某个插件或某个小库,可能用了在论坛上找到的 UIKit 内部方法。检查会根据名称把它找出来。
名称冲突。 你的方法或类恰好和 Apple 的某个私有 API 同名。邮件里已经考虑到这种情况,会要求你改名。Apple 建议给添加到其类上的方法加前缀,比如三个小写字母加一个下划线。
在 macOS 上公开、在 iOS 上不公开的 API。 2024 年,一个 .NET MAUI app 因 _SCDynamicStoreCreate 被拒。Apple 确实有这个函数的文档,但仅限 macOS,而 iOS SDK 的头文件把它标记为 API_UNAVAILABLE(ios, tvos, watchos)。
正面修复,不要绕过
- 更新依赖。 对于 Expo 跟踪的包,根据 Expo CLI 文档,
npx expo install --check会指出与你的 SDK 不匹配的包。其他包请看它们的更新日志。 - 退回上一个版本,如果名称是新版本引入的,并通知开发商。
- 替换或移除已经没人维护的库。
- 给你的方法或类改名,加上前缀。
不要隐藏这个名称。 把字符串拆开拼接,或者绕个弯调用方法,只是把用法藏起来,并没有修复它。这依然违反 2.5.1 条规则和许可协议,而且这时承担风险的是你的账号。
然后,用新的构建号重新构建。远程更新解决不了问题:名称在编译后的原生代码里。上传之前,在新的 IPA 上重新执行第 2 步。名称不应该再出现。
遇到误报怎么办
Apple 的检查有时会出错。2022 年 6 月 6 日,一批 React Native 和 Flutter app 因为 setNavigationDelegate:、isMainFrame 这类 WebKit 公开名称收到了 ITMS-90338,其中包括几乎没改动的更新。第二天,一位 Apple 工程师回复说验证端的问题已解决,请大家重新提交构建。这个讨论串保存在 Expo 的 GitHub 仓库里。
以下三个迹象同时出现时,多半是误报:
- 这次构建和上一次被接受的构建很相似;
- 第 2 步找不到这个名称,或第 3 步显示它在 iOS 上是公开的;
- 同一天有其他开发者在 Apple 论坛上报告了同样的错误。
这种情况下,先看看 Apple 系统状态,然后用新的构建号重新提交。如果错误依旧,把完整的邮件发给 Apple 开发者支持。至于名称冲突,在 Apple 看来不算误报:改名就是了。
下次上传之前
其他 ITMS 错误也有对应的指南:Xcode 版本问题看 ITMS-90725,隐私清单看 ITMS-91053,签名问题看 ITMS-90034。构建被接受之后,在送审之前先用 TestFlight 装上试试。
审核员的拒绝需要更冷静地应对:处理流程在被 Apple 拒绝的 app 页面,最常见的拒绝原因在 Apple 为什么拒绝 app 一文里。
想在上传前检查你的材料,Review Kit 包含我的检查清单,以及会审阅你项目的 Claude Code skills。如果上传卡住了、想找人帮你看看,和我预约一小时:我们视频连线,在你的屏幕上一起过你的项目。如果你正在准备第一次上架,做一次免费诊断:它会帮你理清步骤顺序,并把我的 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 拒绝。

