ITMS-90338: как исправить Non-public API usage в Expo
Автор: Mehdi Azizi
Опубликовано , 9 мин
ITMS-90338: твой билд ссылается на метод, функцию или класс, которые Apple считает приватными. Найди виновную зависимость, обнови её и собери билд заново.
ITMS-90338 выдаёт автоматическая проверка Apple, которая анализирует твой билд. Она сообщает, что бинарник ссылается на API, которые Apple считает приватными: методы Objective-C, функции или классы. Как исправить: найди файл, указанный в письме, выясни, какая зависимость содержит это имя, обнови её или переименуй свой метод, а потом собери билд заново.
В Expo это имя почти всегда приходит из нативной библиотеки и очень редко из твоего JavaScript. 15 октября 2026 года я прогнал команды из этого гайда на симуляторном билде Solys, моего будильника с мини-играми (Expo SDK 57), и на iOS SDK из Xcode 26.2. Ниже расскажу, как читать письмо, как найти виновника и как распознать ложное срабатывание.
Что означает ошибка 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 года Apple ещё предлагает xcrun swift-demangle, чтобы сделать закодированные имена Swift читаемыми.
За этой проверкой стоит guideline 2.5.1: приложение может использовать только публичные API. Лицензионное соглашение Apple Developer Program повторяет это в пункте 3.3.1: никаких вызовов приватных API. Как это правило проверяют живые ревьюеры, подробно разобрано в гайде по guideline 2.5.1. А здесь мы разбираемся с ошибкой при загрузке.
Как и в случае других ошибок ITMS, твоё приложение никто не открывал: Apple проанализировала бинарник во время обработки билда. Apple не публикует список приватных API, поэтому единственный надёжный список даёт тебе само письмо.
Читаем путь: SDK или твой исполняемый файл?
Путь перед списком подсказывает, в каком файле искать.
Frameworks/Name.framework/Name: имя сидит во встроенном динамическом фреймворке, то есть в конкретном SDK. В билде Solys в этой папке 14 фреймворков: Hermes, React Native, модули Expo, которые поставляются уже скомпилированными, и библиотека изображений SDWebImage.- Только имя твоего приложения: имя сидит в главном исполняемом файле. В Expo туда попадает твой нативный код и поды, собранные из исходников (по умолчанию они линкуются статически). Значит, виноватой может оказаться любая нативная библиотека.
Дерево решений: от имени в письме до исправления
Иди по строкам сверху вниз. Первая, которая подходит под твой случай, и даёт подсказку.
| Что ты видишь | Где искать | Исправление |
|---|---|---|
Имя на $s или _$s | xcrun swift-demangle | Пройти дерево заново |
| Нет в твоём IPA | grep по бандлу | Отправить билд заново |
| Имя публичное на iOS | Заголовки iOS SDK | Отправить билд заново |
API_UNAVAILABLE(ios) | Заголовки iOS SDK | Обновить пакет |
В Frameworks/X | SDK, который поставляет X | Обновить SDK |
| Селектор, твоё приложение | Поды и нативные модули | Добавить префикс или обновить |
Символ _nom, твоё приложение | nm -m, потом поды | Заменить вызов |
| Класс, твоё приложение | Твои классы и поды | Добавить префикс к классу |
Строки «Отправить билд заново» означают ложное срабатывание, о нём ниже.
Шаг 1: открыть отправленный билд
Работай с файлом, который реально ушёл в Apple, а не с проектом. Скачай IPA со страницы билда на expo.dev и запусти на 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: найти файл, в котором есть это имя
Начни с широкого поиска. Ищи указанное имя без подчёркиваний в начале по всему бандлу:
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. На фреймворке React из Solys 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
Вот что 15 октября 2026 года с iOS SDK 26.2 дали четыре имени из настоящих писем ITMS-90338:
| Имя из письма | В iOS SDK | Вывод |
|---|---|---|
| _dyld_get_shared_cache_range | Отсутствует | Приватный API |
| boldFontOfSize: | Отсутствует | Приватный или коллизия |
| SCDynamicStoreCreate | API_UNAVAILABLE(ios) | Только macOS |
| isMainFrame | Свойство WebKit | Публичный |
Если имя есть и доступно на iOS, это скорее всего ложное срабатывание. Если имени нет, это приватный API или коллизия имён. Есть два ограничения: публичный API, доступный только в Swift, в этих заголовках не появится, а для аксессора вроде setDelegate: ищи свойство delegate.
Шаг 4: дойти до пакета
Если в пути указан фреймворк, по его имени ты найдёшь под, а затем пакет npm:
grep -n "FrameworkName" ios/Podfile.lock
npm explain package-name
npm explain покажет, какая зависимость его установила, если ты сам его никогда не добавлял.
Если в пути указан исполняемый файл, ищи имя в нативных исходниках, а потом в подах:
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 заодно устанавливает поды. Осторожно: эта команда может пересоздать папку ios. Если ты правил её вручную, работай с копией.
Поиск в ios/Pods важен не меньше, чем поиск по исходникам. В Solys эта папка содержит 15 уже скомпилированных фреймворков в формате .xcframework: модули Expo, Hermes, React Native и сторонние SDK. Поиск только по исходным файлам их код пропустит.
Типичные причины
Сторонний SDK, слишком старый или слишком новый. В сентябре 2026 года Flutter-разработчик сообщил об отказе из-за SDK безопасности: версия 8.2.3 вызывала __dyld_get_shared_cache_range. Версия 8.2.2 проходила, и разработчик SDK посоветовал откатиться на неё, пока не выйдет исправление.
Нативный модуль, который вызывает недокументированный API. Твой собственный модуль, плагин или небольшая библиотека может использовать внутренний метод UIKit, найденный на каком-нибудь форуме. Проверка ловит его по имени.
Коллизия имён. Твой метод или класс случайно называется так же, как приватный API Apple. Письмо это предусматривает и просит переименовать. Apple рекомендует добавлять префикс к методам, которые ты добавляешь в её классы, например три строчные буквы и подчёркивание.
API, публичный на macOS, но не на iOS. В 2024 году приложение на .NET MAUI отклонили из-за _SCDynamicStoreCreate. Apple действительно документирует эту функцию, но только для macOS, а заголовок iOS SDK помечает её как API_UNAVAILABLE(ios, tvos, watchos).
Исправляем, а не обходим
- Обнови зависимость. Для пакетов, которые отслеживает Expo,
npx expo install --checkпокажет те, что не соответствуют твоему SDK, согласно документации Expo CLI. Для остальных читай changelog. - Откатись на предыдущую версию, если имя появилось в новой, и сообщи об этом разработчику.
- Замени или удали заброшенную библиотеку.
- Переименуй свой метод или класс, добавив префикс.
Не маскируй имя. Разбить строку на части или вызвать метод в обход значит спрятать использование, а не исправить его. Это всё равно запрещено правилом 2.5.1 и соглашением, и под удар тогда попадает уже твой аккаунт.
Потом собери билд заново с новым номером билда. Обновления по воздуху недостаточно: имя сидит в скомпилированном нативном коде. Перед отправкой снова выполни шаг 2 на новом IPA. Имени там больше быть не должно.
Когда это ложное срабатывание
Проверка Apple иногда ошибается. 6 июня 2022 года приложения на React Native и Flutter получили ITMS-90338 из-за публичных имён WebKit, например setNavigationDelegate: или isMainFrame, причём даже на почти не изменённых обновлениях. На следующий день инженер Apple написал, что проблему на стороне валидации исправили, и предложил отправить билды заново. Эта ветка сохранилась в GitHub-репозитории Expo.
Если совпали все три признака, это скорее всего ложное срабатывание:
- билд похож на последний принятый;
- шаг 2 не находит имя, или шаг 3 показывает, что оно публичное на iOS;
- другие разработчики в тот же день жалуются на ту же ошибку на форумах Apple.
В этом случае проверь статус сервисов Apple и отправь билд заново с новым номером. Если ошибка не уходит, напиши в поддержку разработчиков Apple и приложи письмо целиком. А вот коллизия имён для Apple ложным срабатыванием не считается: переименовывай.
Перед следующей отправкой
У других ошибок ITMS тоже есть свои гайды: ITMS-90725 про версию Xcode, ITMS-91053 про privacy manifest и ITMS-90034 про подпись. Когда билд примут, установи его через TestFlight до ревью.
Отказы от ревьюера требуют более вдумчивого подхода: порядок действий описан на странице приложение отклонено Apple, а самые частые причины собраны в статье почему Apple отклоняет приложения.
Чтобы проверить всё перед отправкой, в Review Kit есть мой чек-лист и skills для Claude Code, которые проверяют твой проект. Если отправка застряла и тебе нужен взгляд живого человека, забронируй час со мной: разберём твой проект по видеосвязи, прямо на твоём экране. А если ты готовишь первый релиз, пройди бесплатную диагностику: она расставит шаги по порядку и пришлёт тебе мой чек-лист против отказов 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 публичной не считается.
Нужно ли менять номер билда после исправления?
Да. Пусть EAS увеличит его сам: номер, который уже отправлялся для той же версии, могут отклонить с ошибкой ITMS-90189.

