Код написанный на 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).
Выше разобрано что Xcode для библиотек использует framework. Что это?
Что-то типа .jar
в JVM мире, но без архивирования (по сути .framework
это просто папка, с четкой структурой).
Внутри есть аналог манифеста - Info.plist, который содержит метаданные (версия либы и прочая справка для тулинга),
есть сам бинарный файл (то есть уже скомпилированный под конкретную архитектуру и конкретный сдк) и есть заголовочный файл,
чтобы тулинг знал какие вообще классы и методы предоставляет данная библиотека.
Есть тут особенность - бинарный файл может быть разных типов.
- Статическая библиотека - это бинарник, который должен, в процессе линковки с итоговым приложением, вшиться в само приложение.
- Динамическая библиотека - это бинарник, который должен поставляться вместе с итоговым приложением.
Статические библиотеки хороши тем, что код который не нужен итоговому приложению, вообще не будет в приложение вшиваться (типа чистка мертвого кода). А также, статические библиотеки при запуске приложения не добавляют оверхед - их не надо загружать, потому что их код уже в самом бинарнике.
Динамические библиотеки же загружаются при запуске приложения (либо можно определенными усилиями сделать чтобы они загружались только тогда, когда они реально нужны, отсрочив момент загрузки). Когда динамических либ много, и все они грузятся в момент старта приложения - мы получаем статьи как огромные компании переходят с динамики на статику из-за загрузки в несколько секунд и выигрывают некоторое количество этих самых секунд :) Еще динамические либы поставляются как есть, вместе с приложением. Поэтому даже если часть кода этой либы даже не вызываетя приложением этот код всё равно будет занимать место, хоть и никогда не будет вызван. Также можно накосячить и забыть доставить динамическую либу вместе с приложением. Тогда приложение будет крешиться на старте, так как не найдет библиотеку, которую пытается грузить.
Есть еще разница - в работе с ресурсами. формат framework позволяет хранить внутри ресурсы (картинки и прочие файлы), но в случае статической библиотеки эти ресурсы даже не будут автоматически доставлены в само приложение. А вот с динамической библиотекой - ресурсы будут скопированы вместе со всем фреймворком, и фреймворк в процессе своей работы сможет к ним обращаться опираясь на свой бандл (которого у статического фреймворка нет, так как он вшивается в сам апп).
Может сложиться впечатление, что надо использовать статические фреймворки, но не совсем так. При создании статического фреймворка не используется линковщик (потому что ничего линковать и не надо, линкуют именно динамический фреймворк). Поэтому не будет той самой проверки наличия всех необходимых символов. и статический фреймворк - вещь не завершенная. это кусок нечто, что еще только предстоит проверить, но уже на стороне линковки приложения, и тут могут полезть неожиданные ошибки линковки (что какие-то вызовы других библиотек не доступны, например потому что не подключены эти либы к проекту). А вот линамический фреймворк - это завершенный продукт. он и проверен линковщиком, и ресурсы может содержать.
Когда 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. Если переведем - получим "толстый" фреймворк. Это означает что в одном бинарном файле одновременно содержится поддержка нескольких архитектур процессора. В случае айос типичный кейс - arm64 + x64 (для поддержки симулятора на интеле и на apple silicon).
А тут опять развилка. Какие у нас есть варианты:
- собрать из kotlin xcframework, в котором будет сразу поддержка и iphoneos и iphonesimulator и под интел и под силикон. Компиляция займет очень много времени, но полученный xcframework мы можем кидать куда угодно, хоть в сам xcode проект, хоть куда нибудь на хостинг и оттуда через Swift Package Manager / CocoaPods подключать как заранее собранный бинарник. В таком случае разработчик и на силикон маке и на интел маке сможет работать с этой библиотекой, что в симуляторах что на девайсах. Но разработчик не сможет менять Kotlin код и сразу смотреть результат, потому что надо опять пересобирать xcframework (а это сборка 3 вариантов фреймворков, что капец долго) и публиковать куда-то.
- собрать опять же xcframework, но подключать локально - тоже можно, по пути до файла например (мы так делали чтобы swiftui preview не отваливался), но проблема с долгим ожиданием остается - каждая компиляция xcframework это долго.
- собрать framework и также его распространить, можно же? вцелом никто не мешает. но как выше описано - конкретный фреймворк не может работать одновременно на маках интел и силикон, а также на симуляторах и девайсах. Поэтому максимум тут можно опубликовать fat framework с поддержкой симулятора на Intel маках и девайса на arm64. Что в современном мире никому не сдалось. Но сборка будет быстрее чем xcframework конечно.
- динамически выбирать какой вариант framework нам нужен прямо сейчас и собирать только его. Так мы экономим время сборок, но заставляем айосников тащить себе jdk, gradle и kotlin компиляцию. Зато они могут менять kotlin код сколько угодно и относительно быстро получать результат в приложении - xcode согласно специальной Build Phase будет обращаться к Gradle и говорить "вот мои настройки сборки, я щас собираю под симулятор, на эпл силиконе" а градл в ответ соберет ровно тот фреймворк, который нужен в этот момент. После этого xcode забирает полученный framework и линкуется с ним, профит - всё работает.
Так вот реально адевкатных путей 2 из выше описанных:
- распространяем xcframework через SPM/CocoaPods как заранее скомпилированный бинарник, который лежит где-то на хостинге и множество разработчиков просто его скачивают себе да пользуются, как черным ящиком. Менять локально код не получится - это бинарь (также как когда GoogleMaps подключаете себе в проект - вроде либа, а вместо swift кода там файл скомпилированный).
- подключаем через динамическую компиляцию (тут можно напрямую руками билдфазу настроить - "Regular Framework" либо же через CocoaPods) Тогда айосники по сути каждый будет у себя локально на машине производить компиляцию kotlin кода, просто не руками а автоматически xcode с помощью gradle этим займется. Это бьет по времени компиляции, но дает гибкость - может и айосник код менять на локали и просто подтягивать с гита апдейт котлин кода, без выкачивания 40 мегабайтов xcframework'а)
Это менеджер зависимостей созданный комьюнити для Apple - iOS, MacOS.
дополненная версия опубликована на сайте - https://kmm.icerock.dev/learning/kotlin-native/frameworks