Разработка
Сделайте ваше iOS-приложение меньше с помощью динамических фреймворков
Упаковать динамический фреймворк достаточно просто, однако для того, чтобы правильно дедуплицировать ресурсы и сделать приложение легким, вам придется пройти через множество недокументированных шагов.
Каждый junior-разработчик знает о радикальном грехе программного обеспечения:
Однако некоторые из крупнейших iOS-приложений в App Store совершают худший вид этого греха — ненужное повторение целых модулей.
Вот типичный пример. Приложение MyHyundai, которое позволяет водителям легко посмотреть историю обслуживания своего автомобиля и получить помощь на дороге.
Посмотрите на большие красные блоки в этом анализе — они показывают дублирование каталога assets, который копируется в пакете приложения три раза.
Это не только потому, что добрые люди из Hyundai любят файлы .car
. Дело в том, что расширения для iOS, такие как виджеты (MyHyundaiWidget
) и общие расширения (MyHyundaiSharePoi
), находятся в песочнице отдельно от самого приложения.
Поэтому, если вы не очень внимательно относитесь к своей архитектуре, легко совершить ту же ошибку, которую мы видим в MyHyundai: статически связать общую библиотеку пользовательского интерфейса с каждой из ваших целей.
Статические библиотеки, несмотря на то, что якобы являются общим кодом, упаковываются отдельно в скомпилированный бинарник каждой цели (здесь это одно приложение плюс два расширения), что может привести к ненужному дублированию.
Решение из справочника простое — для модулей, разделяемых между целями, подключайте их как динамические фреймворки вместо статических библиотек.
Вместо того чтобы встраивать копию модуля в каждую цель, фреймворки живут независимо в папке Frameworks
пакета .app
, и dyld связывает их с вашим приложением (или расширением) при запуске.
Если вы не знакомы со статическими библиотеками, динамическими фреймворками или dyld, ознакомьтесь с теорией в нашей статье- “Статические и динамические фреймворки на iOS — обсуждение с ChatGPT”.
На практике — особенно если ваше приложение использует современную многомодульную архитектуру с менеджером пакетов Swift — не совсем очевидно, как динамически связывать модули.
Давайте это изменим.
Мы разберем простой учебный проект с открытым исходным кодом EmergeMotors. Мы начнем с немного проблемной папки Before
и будем работать, улучшая архитектуру, до тех пор, пока она не станет соответствовать After
. По ходу дела мы будем анализировать влияние наших изменений на размер приложения.
Появляется EmergeMotors
Вдохновленное MyHyundai, EmergeMotors — это новое крутое приложение для… просмотра фотографий автомобилей. В него входит расширение шаринга и расширение виджета, которые, естественно, также отображают автомобили.
Как и во многих современных приложениях, в EmergeMotors есть специальная библиотека пользовательского интерфейса EmergeUI
, которая содержит общие компоненты и ассеты. Она импортируется во все три цели: приложение, расширение шаринга и расширение виджета.
По чистому совпадению, EmergeMotors имеет ту же архитектурную проблему, что и MyHyundai: утроенный UI-бандл в бинарном файле.
Помимо ассетов, код представления EmergeUI
и подзависимость Lottie также поставляются отдельно с каждым бинарным файлом.
Как уже говорилось выше, практическое решение этой проблемы копирования-вставки заключается в преобразовании статически связанной библиотеки EmergeUI
в динамический фреймворк.
Создание динамического фреймворка с помощью SwiftPM
По умолчанию Xcode выбирает, как компоновать пакет Swift — статически или динамически. На практике он всегда компонует ваши пакеты как статические библиотеки.
Вы можете указать Xcode на динамическое связывание Swift-пакета, указав тип библиотеки пакета как .dynamic
:
// EmergeUI/Package.swift let package = Package( name: "EmergeUI", platforms: [.iOS(.v16)], products: [ .library( name: "EmergeUI", type: .dynamic, targets: ["EmergeUI"]), ], dependencies: [ .package(url: "https://github.com/airbnb/lottie-ios.git", .upToNextMajor(from: "4.4.1")), ], targets: [ .target( name: "EmergeUI", dependencies: [.product(name: "Lottie", package: "lottie-ios")] ) ] )
Ура! Библиотека стала динамичной!
Вы можете проверить, что все получилось, посмотрев на свой основной проект в Xcode.
Если библиотека статическая, то в разделе «Embed» в Frameworks, Libraries, and Embedded Content нет опции, связанной с вашим модулем. Когда вы установите тип библиотеки как динамический, появится выпадающее меню, где вы можете указать, как встроить фреймворк (если он все еще не отображается, принудительно обновите проект через File, Packages, Reset Package Caches).
Убедитесь, что для основного приложения установлен параметр Embed & Sign, который обеспечивает копирование фреймворка в бандл приложения и подписание кода вашим профилем и сертификатом.
Цели расширений должны использовать опцию Do Not Embed, чтобы не создавать дополнительные копии в пакете приложений.
Зонтичные фреймворки
Ваш пакет Swift теперь является динамическим фреймворком.
Помимо обертывания кода, определенного в пакете, подзависимости (включая сторонние библиотеки) теперь являются частью динамически связанного фреймворка, даже если эта подзависимость статична.
С помощью этой техники можно даже обернуть множество библиотек в зонтичный фреймворк и предоставить потребителям единый публичный интерфейс, как будто они импортируют один модуль.
Apple постоянно использует зонтичные фреймворки (import Foundation
, import UIKit
, import AVKit
…), но в целом рекомендуется избегать такого «тяжелого» подхода, если вы не знаете, что делаете.
Первые результаты
Теперь, когда мы определили наш динамический фреймворк в Package.swift
и указали Xcode, как связать его для каждой цели (в Frameworks, Libraries и Embedded content), мы можем заархивировать EmergeMotors и посмотреть, как он выглядит.
Хм… Похоже, нам еще предстоит пройти долгий путь.
Хотя наш общий код библиотеки EmergeUI
и сторонняя зависимость Lottie успешно упакованы как фреймворк, самый тяжелый компонент — EmergeUI.bundle
— все еще упаковывается в каждую цель.
Рассмотрев наш файл xcarchive
напрямую, мы можем заглянуть внутрь бандла .app
(правый щелчок + Show Package Contents) и проверить сам EmergeUI.bundle
.
Каталог ассетов и Lottie JSON упаковываются в пакет и статически связываются с каждой целью. Для модуля с большим количеством ресурсов это сводит на нет большинство преимуществ использования фреймворка.
Если же ваш общий модуль состоит в основном из кода — например, обертка для сторонних зависимостей, внутренних SDK или зонтик для нескольких подмодулей, — то работа проделана отлично. Стандартный подход SwiftPM к созданию динамических фреймворков работает просто фантастически.
К сожалению, если ваш общий код содержит много ассетов, мы столкнулись с серьезным ограничением Swift Package Manager.
Убираем дублирование ассетов
Эту проблему можно решить. Это возможно даже с помощью SwiftPM. Однако это потребует осквернения вашей прекрасно созданной архитектуры пакетов.
Если вы ветеран SwiftUI, вы привыкли погружаться в UIKit, чтобы получить доступ к более сложной функциональности. Техника, которую я собираюсь вам показать, по сути, то же самое, но для архитектурных гиков.
Оговорка: настройка этой системы немного раздражает, а также влечет за собой накладные расходы при каждом обновлении общих ассетов. Поэтому, прежде чем усложнять архитектуру, подумайте, действительно ли вам нужны общие ресурсы в каждой цели. В качестве альтернативы рассмотрите возможность создания отдельных, минимальных модулей ассетов для каждой цели, чтобы свести к минимуму дублирование.
В этой секретной технике нормализации активов есть 4 шага:
- Создайте новый Xcode Framework и перенесите туда общие ассеты.
- Создайте новый пакет Swift с бинарной целью.
- Соберите фреймворк для каждой архитектуры и оберните результаты сборки в xcframework, на который ссылается вышеупомянутая бинарная цель.
- Импортируйте новый пакет в существующую динамическую библиотеку.
Создание фреймворка
Я создал новый проект Xcode под названием EmergeAssets
и перенес в него каталог активов и JSON-ресурсы (не забудьте проверить принадлежность к цели!).
Для лучшей измеряемости я создал эту важную вспомогательную функцию:
public final class BundleGetter { public static func get() -> Bundle { Bundle(for: BundleGetter.self) } }
Это позволяет нам ссылаться на ассеты в пакете EmergeAssets
из других модулей:
// EmergeUI/Sources/EmergeUI/Car/Car.swift import EmergeAssets public struct Car { // ... public var image: Image { Image("(id)", bundle: EmergeAssets.BundleGetter.get()) } }
Импорт бинарной цели
Далее я создал новый Swift-пакет, который я образно назвал EmergeAssetsSPM
.
Как пакет-обертка, его структура очень проста:
let package = Package( name: "EmergeAssetsSPM", products: [ .library( name: "EmergeAssetsSPM", targets: ["EmergeAssetsSPM"]), ], targets: [ .binaryTarget( name: "EmergeAssetsSPM", path: "EmergeAssets.xcframework" ) ] )
Этот binaryTarget
является ключом.
Бинарные цели предварительно компилируются, гарантируя, что ваш пакет ассетов уже аккуратно упакован внутри фреймворка. Это означает, что компилятор не будет собирать его и заново упаковывать в каждую из ваших целей.
Изначально у нас нет никаких файлов в пакете EmergeAssetsSPM
, кроме Package.swift
и этого загадочного сценария оболочки: generate_xcframework.sh
.
Сборка нашего XCFramework
Мы можем использовать инструмент командной строки xcodebuild
для создания бинарного фреймворка.
Я написал сценарий оболочки, который создает локальный фреймворк EmergeAssets
и упаковывает нужные мне варианты архитектуры (iOS + Simulator) в xcframework
, который может быть импортирован в качестве бинарной цели для EmergeAssetsSPM
.
// EmergeAssetsSPM/generate_xcframework.sh # /bin/bash! # Build framework for iOS xcodebuild -project ../EmergeAssets/EmergeAssets.xcodeproj -scheme EmergeAssets -configuration Release -sdk iphoneos BUILD_LIBRARY_FOR_DISTRIBUTION=YES SKIP_INSTALL=NO # Build framework for Simulator xcodebuild -project ../EmergeAssets/EmergeAssets.xcodeproj -scheme EmergeAssets -configuration Release -sdk iphonesimulator BUILD_LIBRARY_FOR_DISTRIBUTION=YES SKIP_INSTALL=NO # To find the Build Products directory, you can either: # 1. Manually build the framework and look in Derived Data # 2. run `xcodebuild -project EmergeAssets.xcodeproj -scheme EmergeAssets -showBuildSettings` and search for BUILT_PRODUCTS_DIR PRODUCTS_DIR=~/Library/Developer/Xcode/DerivedData/EmergeAssets-fuszllvjudzokhdzeyiixzajigdl/Build/Products # Delete the old framework if it exists rm -r EmergeAssets.xcframework # Generate xcframework from build products xcodebuild -create-xcframework -framework $PRODUCTS_DIR/Release-iphoneos/EmergeAssets.framework -framework $PRODUCTS_DIR/Release-iphonesimulator/EmergeAssets.framework -output EmergeAssets.xcframework
Чтобы использовать это самостоятельно, вам нужно позаботиться о включении SDK для всех целевых платформ — убедитесь, что, если вы их поддерживаете, вы включили macosx
, appletvos
, watchos
и соответствующие им симуляторы.
Во время экспериментов с этим отладочные сборки работали нормально, даже если я собирал только релизные конфигурации, но у вас может быть по-разному.
Импорт нашего фреймворка ассетов
Наконец, наш модуль EmergeUI
может импортировать наш фреймворк, обернутый в SwiftPM, как обычную зависимость локального пакета.
// EmergeUI/Package.swift let package = Package( name: "EmergeUI", platforms: [.iOS(.v16)], products: [ .library( name: "EmergeUI", type: .dynamic, targets: ["EmergeUI"]), ], dependencies: [ .package(url: "https://github.com/airbnb/lottie-ios.git", .upToNextMajor(from: "4.4.1")), .package(path: "../EmergeAssetsSPM") ], targets: [ .target( name: "EmergeUI", dependencies: ["EmergeAssetsSPM", .product(name: "Lottie", package: "lottie-ios")] ), .testTarget( name: "EmergeUITests", dependencies: ["EmergeUI"]), ] )
Результаты
После этого довольно существенного архитектурного перехода наш проект собирается. Все три цели (приложение, расширение share и расширение виджета) работают, как и ожидалось.
После архивации и анализа мы видим прекрасную вещь.
Каталог активов (и Lottie JSON) живут счастливой, одинокой жизнью, завернутые в EmergeAssets.framework
. Фреймворк EmergeUI
подключен отдельно, а два плагина расширения едва заметны — они довольно маленькие, когда не копируют все наши активы!
Размер установки уменьшился со значительных 32.3 МБ до прекрасных 13.7 МБ.
Скорость запуска
Я бы не стал рассказывать о динамических фреймворках, не объяснив их недостаток: они могут негативно повлиять на время запуска приложения.
На предварительной фазе запуска приложения dyld связывает необходимые фреймворки с целью, обеспечивая доступ ко всему исполняемому коду и ассетам.
Я провел быстрый анализ производительности между сборками, чтобы оценить, есть ли какое-либо влияние, сгенерировав в процессе несколько интересных графиков.
На этапе <early startup>
dyld связывает динамические фреймворки при запуске. Помимо подключения нашего собственного фреймворка EmergeUI
, dyld также подключает SwiftUI, Foundation и сам Swift!
Ниже приведен профиль запуска приложения для нашего оригинального приложения Before
.
А вот профиль нашего более экономного в плане хранения данных приложения After
.
В данном случае статистически значимых изменений не было обнаружено, что означает, что дополнительная динамическая привязка оказала незначительное влияние на время запуска. Тем не менее, я настоятельно рекомендую вам составить профиль своих собственных приложений, чтобы убедиться, что вы осознаете, на какой компромисс идете.
Заключение
Apple не любит упрощать нам жизнь, не так ли?
Они создали собственную замечательную экосистему пакетов Swift Package Manager, но не приложили много усилий, чтобы объяснить, как использовать ее по максимуму.
Упаковать динамический фреймворк достаточно просто, однако для того, чтобы правильно дедуплицировать ресурсы и сделать приложение легким, вам придется пройти через множество недокументированных шагов.
Но когда вы справитесь с этой задачей, то сможете добиться потрясающих результатов, например, сократить бинарный размер приложения на 58%. Потратьте время на то, чтобы разобраться с проектом-примером, понять эти тайные приемы и применить подобные улучшения в своих собственных приложениях.
-
Новости2 недели назад
Видеозвонки с Лили, Приключения и пианино — обновления Duolingo
-
Новости2 недели назад
Видео и подкасты о мобильной разработке 2024.39
-
Видео и подкасты для разработчиков4 дня назад
Lua – идеальный встраиваемый язык
-
Разработка2 недели назад
Android сломался или я чего-то не понимаю? — Обсуждение на Reddit