Skip to main content

Kotlin frameworks

Как Kotlin работает в iOS

Код написанный на Kotlin компилируется компилятором Kotlin/Native в бинарный файл. Конкретно для iOS этот бинарник заворачивается в .framework - это созданный Apple формат библиотек.

Данный .framework для iOS (а точнее для Xcode) выглядит как обычная нативная библиотека, также как условный Alamofire, SwiftUI и любые другие библиотеки в Apple мире. У этой библиотеки доступен Objective-C header - заголовочный файл, описывающий в синтаксисе Objective-C какие классы и методы есть в этой библиотеке.

.framework должен быть подключен к Xcode проекту. Во первых чтобы Xcode мог прочитать Objective-C header и подсказывать вам что есть такие-то классы и методы, которые были написаны в Kotlin. Во вторых чтобы при компиляции iOS приложения, Xcode смог провести линковку приложения и подключенной библиотеки (линковка это условно говоря когда в вашем коде компилятор видит обращения к методам и классам другой библиотеки, и надо связать бинарник вашего приложения и бинарник этой библиотеки).

Далее, когда приложение скомпилировано и линковка успешно выполнена - приложение можно запустить и приложение будет вызывать и выполнять написанный на Kotlin код, который в результате компиляции превратился в полноценные нативные опкоды конкретной архитектуры цп (поэтому отдельные таски есть на сборку под Arm64 и X64) и с вызовом соответствующих системных методов (за это отвечает сдк - iphoneos / iphonesimulator).

Что такое framework?

Выше разобрано что Xcode для библиотек использует framework. Что это? Что-то типа .jar в JVM мире, но без архивирования (по сути .framework это просто папка, с четкой структурой). Внутри есть аналог манифеста - Info.plist, который содержит метаданные (версия либы и прочая справка для тулинга), есть сам бинарный файл (то есть уже скомпилированный под конкретную архитектуру и конкретный сдк) и есть заголовочный файл, чтобы тулинг знал какие вообще классы и методы предоставляет данная библиотека.

Есть тут особенность - бинарный файл может быть разных типов.

  • Статическая библиотека - это бинарник, который должен, в процессе линковки с итоговым приложением, вшиться в само приложение.
  • Динамическая библиотека - это бинарник, который должен поставляться вместе с итоговым приложением.

Статические библиотеки хороши тем, что код который не нужен итоговому приложению, вообще не будет в приложение вшиваться (типа чистка мертвого кода). А также, статические библиотеки при запуске приложения не добавляют оверхед - их не надо загружать, потому что их код уже в самом бинарнике.

Динамические библиотеки же загружаются при запуске приложения (либо можно определенными усилиями сделать чтобы они загружались только тогда, когда они реально нужны, отсрочив момент загрузки). Когда динамических либ много, и все они грузятся в момент старта приложения - мы получаем статьи как огромные компании переходят с динамики на статику из-за загрузки в несколько секунд и выигрывают некоторое количество этих самых секунд :) Еще динамические либы поставляются как есть, вместе с приложением. Поэтому даже если часть кода этой либы даже не вызывается приложением этот код всё равно будет занимать место, хоть и никогда не будет вызван. Также можно накосячить и забыть доставить динамическую либу вместе с приложением. Тогда приложение будет крешиться на старте, так как не найдет библиотеку, которую пытается грузить.

Есть еще разница - в работе с ресурсами. формат framework позволяет хранить внутри ресурсы (картинки и прочие файлы), но в случае статической библиотеки эти ресурсы даже не будут автоматически доставлены в само приложение. А вот с динамической библиотекой - ресурсы будут скопированы вместе со всем фреймворком, и фреймворк в процессе своей работы сможет к ним обращаться опираясь на свой бандл (которого у статического фреймворка нет, так как он вшивается в сам апп).

С статическими библиотеками есть еще проблема - нельзя вшивать static библиотеку в dynamic, если эта же static будет использоваться в другой dynamic библиотеке или в самом приложении. Как выше уже было сказано - статические библиотеки вшиваются в итоговый бинарник. И вполне реальная ситуация когда статическая библиотека, например, Firebase вшивается и в динамический фреймворк написанный например на Kotlin, и вшивается также в само iOS приложение. В таком случае при запуске iOS приложения в логах будет видно предупреждения говорящие что "найдены дубликаты классов Firebase*** и нельзя предсказать какой экземпляр будет использоваться в каком месте приложения". Это приведет к случайным вылетам и ошибкам, стабильной работы тут ожидать не получится.

Так лучше static или dynamic?

При создании статического фреймворка не используется линковщик (потому что ничего линковать и не надо, линкуют именно динамический фреймворк). Поэтому не будет той самой проверки наличия всех необходимых символов. и статический фреймворк - вещь не завершенная. это кусок нечто, что еще только предстоит проверить, но уже на стороне линковки приложения, и тут могут полезть неожиданные ошибки линковки (что какие-то вызовы других библиотек не доступны, например потому что не подключены эти либы к проекту). А вот динамический фреймворк - это завершенный продукт. он и проверен линковщиком, и ресурсы может содержать.

В Kotlin принято использовать dynamic фреймворки, потому что с ними менее вероятны проблемы, они полностью проверяются на момент сборки Gradle и представляют завершенный продукт. Но если у вас есть достаточное представление о разнице static и dynamic, есть понимание зачем конкретно в вашем проекте нужен static - используйте, плюсы и минусы вы уже прочли выше :)

Что же xcframework?

Когда Apple выпустили Apple Silicon процессоры, у них появилась проблема - теперь на arm64 архитектуре есть и симуляторы айфона и сами айфоны. Решая эту нестыковку они сделали новый форма - xcframework.

По сути xcframework это опять же папка, в которой лежат обычные framework и "манифест" - Info.plist.

В info.plist перечислено какие архитектуры и какие sdk поддерживаются, а также где лежит соответствующий framework.

Наиболее типовой вариант выглядит так:

  • iphoneos - arm64 => лежит framework, ровно такой как и без xcframework, для айфонов, под arm64
  • iphonesimulator - arm64_x64 => лежит fat framework (позже опишу), который под sdk iphonesimulator собран и работает как на apple silicon, так и на старых intel процессорах.

То есть, xcframework, это специальный формат для распространения библиотек, которые поддерживают разные архитектуры и сдк. Там может быть и поддержка сразу и айфонов и маков и вачос - всё в одном xcframework. Но фактически это просто множество разных framework, каждый из которых будет использоваться в нужном случае (для конкретного sdk - свой фреймворк. для айфона, для симулятора айфона, для макоси, для часов и тд)

Что за Fat Framework?

Выше упомянул что есть fat framework. Если переведем - получим "толстый" фреймворк. Это означает что в одном бинарном файле одновременно содержится поддержка нескольких архитектур процессора. В случае айос типичный кейс - arm64 + x64 (для поддержки симулятора на интеле и на apple silicon).

Окей, а что лучше подключать в Xcode то?

А тут опять развилка. Какие у нас есть варианты:

  1. собрать из kotlin xcframework, в котором будет сразу поддержка и iphoneos и iphonesimulator и под интел и под силикон. Компиляция займет очень много времени, но полученный xcframework мы можем кидать куда угодно, хоть в сам xcode проект, хоть куда нибудь на хостинг и оттуда через Swift Package Manager / CocoaPods подключать как заранее собранный бинарник. В таком случае разработчик и на силикон маке и на интел маке сможет работать с этой библиотекой, что в симуляторах что на девайсах. Но разработчик не сможет менять Kotlin код и сразу смотреть результат, потому что надо опять пересобирать xcframework (а это сборка 3 вариантов фреймворков, что капец долго) и публиковать куда-то.
  2. собрать опять же xcframework, но подключать локально - тоже можно, по пути до файла например (мы так делали чтобы swiftui preview не отваливался), но проблема с долгим ожиданием остается - каждая компиляция xcframework это долго.
  3. собрать xcframework локально, но не полный - собирать в нём framework только под 1 архитектуру, которая будет запускаться в данный момент. Подходит если хочется обязательно SPM интеграцию, но публиковать готовый, полный, xcframework на хостинг не хочется (например, чтобы iOS разработчики тоже могли менять Kotlin код и сразу видеть результат). Этот вариант плох тем, что xcframework не полный, в то время как Xcode ожидать будет что в нём есть все нужные ему сдк и архитектуры. Придется вручную запускать скрипт пересборки xcframework под нужную архитектуру и сдк.
  4. собрать framework и также его распространить, можно же? в целом никто не мешает. но как выше описано - конкретный фреймворк не может работать одновременно на macOS Intel и Apple Silicon, а также на симуляторах и девайсах. Поэтому максимум тут можно опубликовать fat framework с поддержкой симулятора на Intel маках и девайса на arm64. Что в современном мире никому не сдалось. Но сборка будет быстрее чем xcframework конечно.
  5. динамически выбирать какой вариант framework нам нужен прямо сейчас и собирать только его. Так мы экономим время сборок, но заставляем айосников тащить себе jdk, gradle и kotlin компиляцию. Зато они могут менять kotlin код сколько угодно и относительно быстро получать результат в приложении - xcode согласно специальной Build Phase будет обращаться к Gradle и говорить "вот мои настройки сборки, я щас собираю под симулятор, на эпл силиконе", а gradle в ответ соберет ровно тот фреймворк, который нужен в этот момент. После этого xcode забирает полученный framework и линкуется с ним, профит - всё работает.

Так вот реально адекватных путей 2 из выше описанных:

  1. распространяем xcframework через SPM/CocoaPods как заранее скомпилированный бинарник, который лежит где-то на хостинге и множество разработчиков просто его скачивают себе да пользуются, как черным ящиком. Менять локально код не получится - это бинарь (также как когда GoogleMaps подключаете себе в проект - вроде либа, а вместо swift кода там файл скомпилированный).
  2. подключаем через динамическую компиляцию (тут можно напрямую руками билдфазу настроить - "Regular Framework" либо же через CocoaPods) Тогда айосники по сути каждый будет у себя локально на машине производить компиляцию kotlin кода, просто не руками а автоматически xcode с помощью gradle этим займется. Это бьет по времени компиляции, но дает гибкость - может и айосник код менять на локали и просто подтягивать с гита апдейт котлин кода, без выкачивания 40 мегабайтов xcframework'а)