Всем привет!

Меня зовут Антон. Я iOS разработчик в платформенной команде Циан.

С релизом Xcode 14.3 Apple убирает поддержку Rosetta и почти прямым текстом говорит разработчикам, что откладывать адаптацию своих проектов под Apple Silicon дальше уже некуда. Сегодня я поделюсь историей про то, с какими сложностями мы столкнулись в нашем основном приложении Циан, и какое стороннее решение помогло нам избавиться от Rosetta.

Конечно же, будут данные, графики и результаты, которые получили в процессе работы. В общем, всё как мы любим. Поехали!

Что такое Rosetta?

Для начала давайте коротко вспомним, что такое Rosetta?

Компания Apple за свою историю несколько раз меняла архитектуру процессоров в своих компьютерах. Это было в 1994 году, когда Apple перешли с процессоров Motorola на RISC-архитектуру PowerPC. Далее это было в 2006 году, когда Apple перешли от использования PowerPC к архитектуре Intel x86. И, наконец, это случилось в 2020 году, когда были представлены процессоры Apple Silicon, и начался переход от Intel x86 к архитектуре ARM.

У всех этих архитектур есть собственные наборы процессорных команд, которые отличаются между собой. Так вот, чтобы разработчикам, как мы с вами, было легче жить в эпоху перемен, Apple выпустили транслятор, который бы позволял переводить набор команд от одной архитектуры в набор команд другой архитектуры во время работы приложений. Этот транслятор и назвали Rosetta.

С одной стороны Rosetta несёт, несомненно, пользу. Пока вы не адаптировали свой проект под новую архитектуру. С другой стороны, на транслирование команд из их одной архитектуры в другую тратятся ресурсы, время при сборке и запуске проекта и лишает вас возможности перейти на Xcode 14.3, использование которого спустя некоторое время станет обязательным условием для публикации в AppStore.

Как понять, что используется Rosetta?

Теперь давайте разберёмся, а как вообще понять, что при запуске вашего приложения на машине с процессором Apple Silicon используется Rosetta? Это можно сделать несколькими способами.

Самый простой — воспользоваться системной утилитой Activity Monitor, которая идёт в составе macOS. В ней на вкладке CPU можно увидеть список запущенных процессов. Среди них будет и ваше приложение, которое запущено на симуляторе. В колонке Kind (или Architecture в более ранних версиях macOS) будет одно из двух значений — Intel или Apple. Если у процесса вашего приложения указано Apple, то поздравляю — вы не используете Rosetta. Значение Intel говорит, соответственно, об обратном.

Ещё есть способ узнать эту же информацию из консоли:

ps -p `fuser /usr/libexec/rosetta/runtime | sed -e 's/.*: //' | sed -e 's/ /,/g'`

В результате будут выведены запущенные процессы приложений, которые взаимодействуют с Rosetta:

/usr/libexec/rosetta/runtime:
  PID TTY       	TIME CMD
 7240 ??     	4:49.47 /Applications/Xcode.app/Contents/MacOS/Xcode

Как было в Циан?

Мы разобрались, что такое Rosetta, и как определить, использует ли этот транслятор ваше приложение. Теперь расскажу, как было у нас в Циан совсем недавно.

В 2022 году мы начали активно обновлять парк рабочих машин, заменяя Intel на Apple Silicon.

Вот немного статистики про сам проект:

  • 20 разработчиков;

  • 680 тысяч строк кода (500 тысяч на Swift и 180 тысяч на ObjC);

  • 55 сторонних зависимостей.

Конечно же, при работе над проектом нам хотелось использовать мощность новых машин по полной. Первые из нас, кто получил такие машины, очевидно, не смогли с первого раза собрать проект и сделали хак, включив опцию Open using Rosetta для Xcode и iOS Simulator.

Время холодной сборки проекта на машине с M1 Pro и 16Gb RAM на тот момент составляло:

  • для симулятора: 620 секунд;

  • для реального устройства: 600 секунд.

Что делали?

Мы обновили все сторонние зависимости, в которых появилась поддержка Apple Silicon.

Нам пришлось подправить немного код в тех местах, где мы инициализировали различные компоненты. Например, у нас используются карты от Яндекса, и для их корректной работы на симуляторе нам пришлось добавить следующую обёртку:

#if TARGET_IPHONE_SIMULATOR && TARGET_CPU_ARM64
    	let mapView = YMKMapView(frame: .zero, vulkanPreferred: true)
#else
    	let mapView = YMKMapView()
#endif

Обратите внимание на опцию vulkanPreferred. Пример того, как разработчики адаптируют свои библиотеки под Apple Silicon.

Ещё выключили опцию Open using Rosetta и добавили настройку EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64 в проект.

Мы подумали, что это не избавит нас от Rosetta полностью, но хотя бы сэкономит время на сборке тем разработчикам, кто отлаживается на реальных устройствах.

Тут надо понимать, что даже если вы выключите опцию Open using Rosetta и добавите опцию про исключения arm64 из архитектур для симулятора, то Rosetta всё равно будет использоваться, и в том же Activity Monitor вы будете видеть значение Intel.

После этих изменений время сборки на девайсе составило 414 секунд.

Спустя некоторое время мы попытались избавиться уже от ранее добавленной опции про исключение arm64 из поддерживаемых архитектур для симулятора.

И тут нас ждало главное приключение в этой истории. Помните, я вам рассказывал, что мы обновили все зависимости, которые добавили поддержку Apple Silicon? На самом деле я вас немного обманул. Одну зависимость обновить мы не смогли.

Среди прочих сторонних библиотек и framework-ов у нас образовался следующий «любовный треугольник»:

С одной стороны, у нас в приложении используется YooKassaPayments для проведения оплаты с различных банковских карт и Apple Pay. С другой стороны, с помощью YandexMobileAds мы показываем различную рекламу. Обе эти зависимости требуют для своей работы YandexMobileMetrica. Все три зависимости поддерживают Apple Silicon в своих актуальных версиях. Но в чём тогда загвоздка?

А загвоздка в том, что для работы YooKassaPayments требуется динамическая версия YandexMobileMetrica. А для работы YandexMobileAds актуальной версии 5.x, в которой добавлена поддержка Apple Silicon, требуется статическая версия YandexMobileMetrica. У нас же в проекте использовалась YandexMobileAds версии 4.x, в которой есть возможность работы с динамической версией YandexMobileMetrica. Но в ней нет поддержки Apple Silicon, и она поставляется в виде обычного framework-а, а не в новом формате xcframework.

Соответственно, как только мы убрали опцию EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64 для симуляторов, компилятор сообщил, что YandexMobileAds не поддерживает запуск на Apple Silicon.

Мы завели issue на GitHub с описанием проблемы для актуальной версии YandexMobileAds и стали думать, что делать с этой проблемой.

Конечно, у нас был вариант исключить код, который отвечает за показ рекламы, для симулятора. Но тогда получалось, что для разработки и отладки некоторых фичей нам было никак не обойтись без девайса. Не такая уж и большая проблема, но всё же нужно было держать в голове, что часть функционала приложения не будет доступна на симуляторе.

Решили поискать ещё альтернативные пути решения проблемы. И натолкнулись на open source утилиту под названием arm64-to-sim.

В чем её суть: если какой-либо бинарник поддерживает архитектуру arm64 и может работать на девайсе, то с помощью данной утилиты можно сделать версию бинарника, которая подходит для запуска в симуляторе на машинах с Apple Silicon.

Что за чёрная магия, спросите вы? На самом деле никакой магии нет. Тут нужно немного сделать шаг в сторону и погрузиться в то, как устроен бинарный файл формата Mach-O, который применяется во всех операционных системах Apple.

Я не буду сильно вас грузить теорией и всеми нюансами этого формата. Для наглядности на картинке ниже показана упрощённая схема. 

Тут для нас самое интересное — секция с командами загрузки Load Commands.

Во время сборки проекта одним из этапов является линковка библиотек и framework-ов, которые нужны для работы вашего приложения. За это отвечает утилита ld или, другими словами, linker. Так вот, этот линкер читает каждый бинарный файл и выполняет по очереди команды загрузки, которые объявлены в заголовке нашего бинарного файла.

Оказывается, что разница между бинарным файлом, который может работать в симуляторе на машине с Apple Silicon, и бинарным файлом, который может работать на обычном arm64-девайсе, ровно в одной загрузочной команде из заголовка. Более того, обе эти команды ссылаются на одни и те же сегменты данных.

Так вот, возвращаясь к работе утилиты. Она умеет искать нужную загрузочную  команду в переданном ей файле и подменять её на нужную команду для запуска на симуляторе с Apple Silicon.

Если интересно погрузиться в эту тему, то разработчики утилиты написали две крутые статьи (раз, два), в которых всё подробно написано об особенностях работы со статическими и динамическими framework-ами.

Теперь давайте вернёмся к нашему любовному треугольнику из сторонних зависимостей. В итоге мы воспользовались arm64-to-sim и перепаковали обычный framework YandexMobileAds в настоящий xcframework с поддержкой Apple Silicon. Приложение стало запускаться на симуляторе.

Но на этом наши приключения не закончились. 

При переходе на любой экран, где у нас отображалась карта, приложение начало падать. Помните я говорил, как нам пришлось добавить обёртку для Яндекс карт, в которой использовались макросы TARGET_IPHONE_SIMULATOR и TARGET_CPU_ARM64?

Оказалось, что TARGET_IPHONE_SIMULATOR уже давно отмечен как deprecated (да, тут мы оказались немного староверами и упустили момент), и работал он ровно до того момента, пока мы использовали Rosetta. При сборке проекта для симулятора на Apple Silicon без Rosetta макрос TARGET_IPHONE_SIMULATOR выдает false. Решение тут достаточно очевидное. Достаточно перейти на более актуальный способ проверки окружения с помощью targetEnvironment:

 #if targetEnvironment(simulator)

Теперь при запуске проекта на Xcode 13 приложение не падало и казалось, что это победа.

Но тут, внезапно, подкрался Xcode 14, который успели установить некоторые разработчики из команды, и проблема с падением приложения при переходе на экраны с картой опять вернулась.

Падение выглядело следующим образом: главный поток, EXEC_BAD_ACCESS, ноль полезной информации в логах. Всё, как мы любим. Тут на помощь пришла диагностическая опция Zombie Objects из Memory Management. Это позволило в логах в момент падения увидеть основную причину:

*** -[CaptureMTLCaptureScope release]: message sent to deallocated instance 0x280aafb40

Я немного повторюсь и ещё раз спрошу, помните вот ту обертку с Яндекс картами? Там была опция vulkanPreferred. Так вот, Vulkan — это кроссплатформенное API для  отрисовки 2D- и 3D-графики, которое приходит сейчас на смену OpenGL. Судя по stacktrace-ам, Яндекс карты взаимодействуют с Vulkan API через библиотеку MoltenVK, которая, в свою очередь, на iOS взаимодействует уже с нативным Metal API. И класс CaptureMTLCaptureScope из лога падения как раз является частью Metal API. Он отвечает за «захват» Metal-фреймов c GPU.

Это, конечно, всё увлекательно и интересно, но что нам делать? Падение внутри системного API в результате вызова из библиотеки, к исходному коду которой у нас нет доступа... Звучит так себе.

Тут нам на помощь пришёл changelog для Xcode 14, в котором был следующий пункт в разделе изменений для Metal:

Known Issues
	Profiling Metal captures containing mesh pipelines is disabled. (93255574)

Стало ясно, что парни из Купертино что-то отломали в части захвата фреймов с GPU.

К моменту, когда мы столкнулись с этой проблемой, был уже доступен Xcode 14.1 RC 2. Но и в текущем актуальном релизе Xcode 14.2 проблема сохраняется.

В итоге, чтобы избавиться от этого падения, мы решили отключить возможность захвата GPU-фреймов. Это можно сделать в параметрах схемы таргета в Xсode в разделе Options. Там есть настройка GPU Frame Capture, которой мы выставили значение disabled.

Тут важно понимать, что это именно отключение захвата GPU frame-ов. На возможность отображения иерархии view при отладке это никак не влияет. На них просто не будут отображаться фреймы, для которых используется Metal API. То есть для нас это вьюшки с картами.

Это позволило нам собрать проект без Rosetta на Xcode 14.

После этих изменения время сборки на симуляторе составило 405 секунд.

Points scored

К чему пришли?

В итоге все эти приключения позволили нам избавиться от Rosetta в проекте. Время чистой сборки сократилось c 10 минут до 6.5—7 минут, т.е. на 30—35%. Это позволило сэкономить время не только разработчикам, но и нашим QA, так как время на прогон Unit- и UI-тестов сократилось и сборки до них стали долетать быстрее.

Points scored
Points scored

В завершение хочу сказать, что если какая-то сторонняя зависимость мешает вам отказаться от Rosetta, то не стоит надеяться и ждать, когда авторы адаптируют её под Apple Silicon. Есть инструмент под названием arm64-to-sim, который позволит это сделать самостоятельно.

Надеюсь, наша история вам понравилась, и вы нашли в ней что-то полезное для себя.

Если у вас остались вопросы, то не стесняйтесь их задавать в комментариях.

Всем спасибо!

Комментарии (4)


  1. GaryKomarov
    00.00.0000 00:00

    Эпоха хакинтоша для запуска xcode закончилась?


    1. Tomcattoff Автор
      00.00.0000 00:00

      Не то чтобы прям закончилась, но близится к окончанию, на мой взгляд. Apple все же еще поддерживает Intel-based машины, поэтому и версии Xcode для них еще будут выходить.


      1. GaryKomarov
        00.00.0000 00:00

        А когда перестанут Xcode для Intel обновлять.

        Придется для сборки iOS приложений добывать яблочную железку на ARM?
        Или все же можно как то извратиться с эмулятором?


        1. Tomcattoff Автор
          00.00.0000 00:00

          Последние машины на Intel были выпущены в 2019 году. Apple обычно поддерживает свои продукты около 7 лет. Так что еще год-два скорее всего будут обновлять.

          Я не специалист по хакинтошу, но предполагаю, что с большой долей вероятности потребуется яблочная железка.