Инженеры Apple придумали прекрасные по быстродействию и производительности процессоры Apple Silicon (M1, M1 Max и так далее) на архитектуре arm64. Но за полученное быстродействие разработчикам пришлось платить своим временем.
Я — Никита Коробейников, iOS Team Lead в Surf. Расскажу, к каким проблемам мог привести апгрейд рабочего мака и что нужно учитывать с изобретением процессоров Apple Silicon.
Статья вдохновлена ограничениями в недавно вышедшем Xcode 14.3: запуск из-под Rosetta в нём стал deprecated.
Проблема
Однажды вы обновили свой мак и собирались приступить к выполнению рабочих задач. В вашем TODO-листе было несколько задачек на перекрашивание кнопок: типичный рабочий день iOS-разработчика. Вы были немного воодушевлены лишь тем, что на выходных к вам пришел новый Mac mini на M1.
Вы ожидали что проект будет собираться быстрее, вы выполните поставленные задачи и сможете наконец отрефакторить легаси-код в освободившееся время, но что-то пошло не так… проект не собрался на симулятор.
Чем же вызвана эта ошибка? Дело в том, что маки с процессорами от Apple работают на архитектуре arm64, а старые маки с процессорами от Intel — на архитектуре x86_64.
Можно сказать, что архитектура определяет словарь машинного языка — совместимость с определенным набором команд. При компиляции написанный код переводится на машинный язык. Получается, что при сборке проекта на симулятор Xcode обнаружил несовместимость с набором команд архитектуры arm64 и симулятора iOS.
Временное решение
Решение было найдено быстро. На знаменитом Stack Overflow обладатели новеньких маков пришли на выручку друг другу.
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
Но со временем разработчики начали замечать проблемы.
Например:
необычно быстрый скролл при использовании SDK карт,
падения при прогоне UI-тестов.
Тем не менее это решение по-своему актуально: оно позволяет производить отладку приложения на симуляторе.
Откуда появляются проблемы
Исключая arm64 из списка архитектур, мы собираем пакет лишь под x86_64 — то есть архитектуру Intel-процессоров. Новые маки могут запускать программы x86_64 через динамический транслятор — Rosetta. Он обеспечивает совместимость со старыми десктопными приложениями. Без него маки с процессорами Apple Silicon на старте продаж имели бы в арсенале лишь системные приложения, поддерживаемые самой компанией Apple.
Проверить, как запустилась программа, можно через мониторинг системы — Activity Monitor. Если процесс запущен без транслятора Rosetta, в графе Kind будет Apple. Иначе — Intel. На старых маках эта графа попросту отсутствует.
Сейчас уже много сторонних приложений адаптированы под arm64 и запускаются нативно. Замечательно, что MacOS сам определяет, как должно быть запущено то или иное приложение. Однако эта самостоятельность системы приводит к тому, что становится неочевидно, как повлиять на запуск симулятора.
Чтобы симулятор на маках с процессорами Apple Silicon запускался без транслятора, разрабатываемые приложения и их зависимости должны быть адаптированы под архитектуру arm64.
Полноценное решение: XCFramework
Для решения проблем с упаковыванием сборок для разных таргетов под разные архитектуры был создан новый формат упаковки — XCFramework.
Как работает XCFramework
XCFramework представляет упорядоченную структуру папок и спецификацию в Info.plist файле. Эта структура позволяет упаковать в одном файле сочетание платформ и архитектур, несовместимое в старых форматах.
Перейдём к примеру.
На картинке — пакет XCFramework библиотеки JRE (Java Runtime Environment). Она является частью официального релиза утилиты j2objc, позволяющей переводить Java-код на Objective-C. К слову, именно этот пакет вынуждал MacOS запускать проект через Rosetta. В спецификации видно, что пакет содержит библиотеку для симулятора лишь с x86_64 архитектурой (см. AvailableLibraries/Item 0/SupportedArchitectures).
На следующей картинке — та же зависимость JRE, но уже с полным необходимым набором архитектур для симулятора. Собрана вручную с того же официального релиза версии 2.8.
Сборка XCFramework из архивов
Перед упаковкой XCFramework требуется подготовить архивы с фреймворками.
Этот способ сборки возможен, если у вас есть доступ к исходникам проблемной зависимости.
Сборка архива с фреймворком:
xcodebuild archive \
-project "${project}" \
-scheme "${scheme}" \
-sdk iphonesimulator \
-archivePath "archives/${scheme}-sim" \
ONLY_ACTIVE_ARCH=NO \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
Флаг ONLY_ACTIVE_ARCH=NO
позволяет включить в сборку все валидные архитектуры. Для каждой платформы существует свой набор валидных архитектур, который по умолчанию содержится в Xcode. Например, для симулятора это x86_64 и arm64. Для актуальных айфонов — arm64. Для часов и appleTV будут другие значения.
Флаг BUILD_LIBRARY_FOR_DISTRIBUTION=YES
необходим для подготовки к дистрибьюции архива через XCFramewrok.
Создание XCFramework из двух архивов фреймворка:
xcodebuild -create-xcframework \
-framework "${archive_path}/${name}-ios.xcarchive/Products/Library/Frameworks/${name}.framework" \
-framework "${archive_path}/${name}-sim.xcarchive/Products/Library/Frameworks/${name}.framework" \
-output "${path}/${name}.xcframework"
Команда create-xcframework
упакует несколько .framework в один XCFramework.
Создание XCFramework из двух статических библиотек:
xcodebuild -create-xcframework \
-library $result_dir/simulator-fat/${lib_name}.a \
-library $result_dir/device/${lib_name}.a \
-output $result_dir/${framework_name}.xcframework
Вместо .framework могут быть использованы статические библиотеки .a.
При необходимости можно также добавить публичные заголовки и dSym-файлы.
Мы показали, как собрать XCFramework, имея исходники. Но иногда проект может зависеть от статической библиотеки, доступ к исходникам которой отстутствует. Или библиотека устарела: это затрудняет её пересборку.
Проблема со статическими библиотеками
Статические библиотеки распространяются в скомпилированном виде, то есть уже переведены на машинный язык. Динамические переводятся в момент компиляции. Ошибка, которую мы обсуждаем, возникает именно при подключении в проект статической библиотеки — в файлах .a и .xcframework.
Как же распаковать то, что уже упаковано, и добавить привязку платформы iOS-симулятора к архитектуре arm64? Файлы с расширениями .a и .xcframework — это лишь способ представления и упаковки библиотек в Linux системах. Статическая библиотека может быть:
thin (худой) — содержит файлы, привязанные к одной архитектуре.
fat (полной) — содержит несколько thin-файлов.
Для упрощения будем называть thin и fat-файлы в формате {платформа}_{архитектура/ы}.
Нам надо посмотреть информацию о внутренних библиотеках статического фреймворка: для этого есть lipo. Это мощный инструмент, позволяющий анализировать, создавать и извлекать содержимое статических библиотек.
Посмотреть информацию о внутренних библиотеках поможет команда lipo -info.
% lipo -info libjre_emul.a
Architectures in the fat file: libjre_emul.a are: arm64
% lipo -info libjre_emul.a
Architectures in the fat file: libjre_emul.a are: x86_64 arm64
Извлечь одну из thin-библиотек из fat-файла можно с помощью команды lipo -thin.
lipo -thin arm64 Example.framework/Example -output Example.arm64
Однако fat-библиотека не может содержать файлы, привязанные к одной и той же архитектуре. Иными словами, iphone_arm64 и ios_simulator_arm64 несовместимы в одном .a-файле.
При этом они совместимы в XCFramework: просто нужно упаковать сборки для симулятора в одном fat-файле, а сборки для айфона — а в другом.
План готов, но чего-то не хватает. Откуда взять сборку ios_simulator_arm64 без исходников? Можно переопределить привязку библиотеки iphone_arm64, но нам придётся познакомиться с ещё одним инструментом.
arm64_to_sim нас спасёт
Для начала надо извлечь все файлы из подопытной iphone_arm64. Используем штатный архиватор.
Разархивирование библиотеки ‘.a’:
ar x ${lib_name}.a
Затем с помощью arm64_to_sim патчим каждый .o файл (Compiled Object Format), включая вложенные файлы. Платформа iphone меняется на ios_simulator.
Привязка всех файлов под симулятор (синтаксис shell-zsh):
for file in ./*.o **/*.o ; do
echo "Patching file: - ${file}";
arm64-to-sim $file;
done;
Собираем результат в библиотеку. Упаковка файлов в библиотеку ‘.a’:
ar crv ${archive_output_dir}/${archive_name}.a ${archive_input_dir}/**/*.o
И формируем fat-библиотеку для симулятора.
lipo -create -output $fat_output_dir/simulator-fat/${fat_lib_name}.a
${fat_output_dir}/simulator-arm64/${fat_lib_name}.a
${fat_output_dir}/simulator-64/${fat_lib_name}.a
Готово. Таким образом можно пропатчить любую статическую библиотеку, положить ее в XCFramework, как описано выше и… всё. Ваш проект будет окончательно адаптирован. Проблемы с отладкой на симуляторе уйдут, когда вы отключите временное решение с настройкой EXCLUDED_ARCH.
С тех пор, как Apple начали выпускать маки на процессорах собственного производства, прошло уже много времени, поэтому многие разработчики библиотек уже пересобрали свои фреймворки в виде XCFramework с нужным набором архитектур. Однако по-прежнему можно наткнуться на неадаптированные библиотеки.
По возможности избегайте использования флага EXCLUDED_ARCH для решения проблемы, описанной в статье, и попробуйте просто обновить зависимости. Если же обновление невозможно, вам может помочь переопределение привязки с arm64_to_sim.
Источники
Больше полезного про iOS — в нашем телеграм-канале Surf iOS Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf. Присоединяйтесь >>