GrabDuck

Как собрать собственный фреймворк для iOS

:

Среди задач мобильного разработчика, помимо самой частой (написания, собственно, приложений) периодически появляется и такая, как создание sdk.

Примерами такой задачи может быть создание sdk, использующего REST API какого-либо сервиса (реклама, аналитика, погода), библиотека реализаций алгоритмов, обработка изображений… Список практически неограничен.

Ошибки, допущенные в таком продукте, исправлять гораздо сложнее, чем при разработке приложений. В случае с приложением достаточно обновить его в AppStore, дождаться прохождения модерации и обновления пользователем. В случае же с sdk цепочка прирастает дополнительным шагом — дождаться его обновления разработчиком.

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

Любой подобный sdk обычно состоит из многих компонент: библиотеки, тестового приложения, документации, плагинов к другим инструментам. В этой статье я расскажу о сборке библиотеки в виде фреймворка, некоторых приёмах и особенностях разработки.



Для начала, небольшая ремарка о решаемой задаче. Наша цель — получить библиотеку в таком формате, который требует минимум усилий и знаний при интеграции, а также не имеет побочных эффектов после неё.
Таким решением может стать, к примеру, CocoaPods, который, в частности, позволяет распространять пакеты с закрытым исходным кодом. Существует достаточно много статей (например здесь или здесь), описывающих, как публиковать свои библиотеки в CocoaPods, и повторяться мы, пожалуй, не будем.
Для нашего sdk, рассчитанного на широкий круг разработчиков, такого способа распространения, очевидно, недостаточно: часть разработчиков не пользуются им принципиально («а вдруг опять github заблокируют?»), часть же не слышали о нем вовсе.
Сфокусируемся на сборке sdk в формате фреймворка. Всё, что понадобится пользователю для интеграции — перетащить его к себе в проект.

Про динамические фреймворки iOS8


С выходом iOS 8 и соответствующей ей версии iOS SDK появился новый тип таргета при создании проекта — динамический фреймворк. Попробуем выяснить, подходит ли он нам.
Появление такого типа таргета было обусловлено, в первую очередь, введением extension’ов, с которыми основному приложению приходится разделять общий код и ресурсы. Получающиеся в итоге динамические библиотеки похожи на настоящие, которые присутствуют, например, в OS X лишь отчасти. Дело в том, что библиотека по-прежнему остаётся в песочнице приложения и может быть использована только для extension’ов и основного приложения.
А можно ли использовать такие фреймворки для распространения нашего sdk? В данный момент ситуация неоднозначна. Из документации Apple следует, что embedded framework возможно использовать только на iOS8. Приложение всё ещё может иметь Deployment Target ниже, но использовать такой фреймворк не удастся. Практика же показывает несколько иную ситуацию. Embedded framework всё же удаётся запустить на девайсах с версией iOS ниже 8, однако эта радость быстро проходит при попытке публикации приложения, использующего такой фреймворк в AppStore. На этапе автоматической валидации выдаётся ошибка:
The MinimumOSVersion of framework "..." is invalid. The minimum value is iOS 8.0;

Установка минимальной версии для таргета фреймворка в 8.0 приводит, в свою очередь, к ошибке на этапе линковки проекта:
ld: embedded dylibs/frameworks are only supported on iOS 8.0 and later

Резюме по динамическим фреймворкам на данный момент такое: использовать и распространять их возможно, но только если минимальная версия iOS SDK начинается с 8.

Про сборку SDK


Попробуем воспользоваться тем же шаблоном динамического фреймворка, который предоставляет Xcode 6, но теперь уже для сборки его статической версии. Оказывается, после некоторых модификаций это вполне возможно. Приступим.

Создадим новый проект, где в качестве типа таргета выберем Cocoa Touch Framework. Имя выбираем любое. Для определённости, наш назовём MySDK.

Добавляем в проект классы нашего sdk. Заголовочные файлы, которые должны быть публично доступны, помечаем как Public и импортируем в umbrella-header, который по умолчанию был создан с проектом. В нашем случае он называется MySDK.h

Далее, заходим в BuildSettings нашего таргета и в секции Linking меняем Mach-O Type на Static Library.

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


Разработчик, использующий нашу библиотеку, наверняка захочет запускать код на всех возможных для iOS архитектурах процессоров. И мы должны об этом позаботиться. На данный момент актуальный список архитектур включает 3 для устройств (armv7, armv7s, arm64) и 2 для симуляторов (i386, x86_64). Для этого необходимо собрать так называемую fat binary библиотеку, которая будет включать в себя слои для каждой из поддерживаемых архитектур. Для этого создадим в проекте таргет типа Aggregate. Назовём его MySDKUniversal.

В BuildPhases таргета добавляем фазу Run Script со следующим скриптом:

FRAMEWORK_NAME="${PROJECT_NAME}"
SIMULATOR_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${FRAMEWORK_NAME}.framework"
DEVICE_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FRAMEWORK_NAME}.framework"
UNIVERSAL_LIBRARY_DIR="${BUILD_DIR}/${CONFIGURATION}-iphoneuniversal"
FRAMEWORK_PATH="${UNIVERSAL_LIBRARY_DIR}/${FRAMEWORK_NAME}.framework"

xcodebuild -project ${PROJECT_NAME}.xcodeproj -sdk iphonesimulator -target ${FRAMEWORK_NAME} -configuration ${CONFIGURATION} clean build CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphonesimulator ARCHS='i386 x86_64'

xcodebuild -project ${PROJECT_NAME}.xcodeproj -sdk iphoneos -target ${FRAMEWORK_NAME} -configuration ${CONFIGURATION} clean build CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphoneos ARCHS='arm64 armv7 armv7s'

rm -rf "${UNIVERSAL_LIBRARY_DIR}"
mkdir "${UNIVERSAL_LIBRARY_DIR}"
mkdir "${FRAMEWORK_PATH}"
cp -r "${DEVICE_LIBRARY_PATH}/." "${FRAMEWORK_PATH}"
lipo "${SIMULATOR_LIBRARY_PATH}/${FRAMEWORK_NAME}" "${DEVICE_LIBRARY_PATH}/${FRAMEWORK_NAME}" -create -output "${FRAMEWORK_PATH}/${FRAMEWORK_NAME}"

Здесь мы дважды вызываем сборку нашего фреймворка и собираем получившиеся библиотеки в один fat binary с помощью утилиты lipo. Обратите внимание на последний параметр ARCHS при вызове xcodebuild. Если его не указать, то сборка для девайса будет использовать список архитектур по-умолчанию описанный в переменной ARCHS_STANDARD. Дело в том, что Apple, судя по всему, намеренно исключила из переменной ARCHS_STANDARD armv7s для Xcode6, оставив только armv7, arm64. В целом, в этом нет большой проблемы для разработчика, и в приложении он практически всегда может исключить armv7s. Приложение без особых проблем, хотя, вероятно, и с меньшей оптимизацией, будет работать на процессорах A6, A6X. Но, разрабатывая sdk, не стоит принимать такое решение за разработчика. Гораздо проще перечислить список архитектур явно.

Про зависимости


Редко какой код сможет обойтись без сторонних зависимостей. Например, библиотеки работы с сетью, различные парсеры, категории к классам системных фреймворков и т.д. В итоге, все эти классы попадают в наш sdk, и разработчик, который его использует, сможет столкнуться с проблемами при сборке своего проекта. Эти проблемы начнутся как только он захочет использовать одну из тех «вшитых» в sdk библиотек. Компилятор выдаст кучу “duplicate symbols” при сборке его приложения. Казалось бы, что в этом плохого: разработчику всего лишь нужно удалить из зависимостей эту библиотеку, так как её классы уже есть в нашем sdk. Загвоздка в том, что он лишается возможности обновлять эту библиотеку и становится зависим от наличия нашего sdk.

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

Добавляем все наши зависимости в раздел CompileSources списка фаз сборки. Последней фазой добавляем кастомный скрипт. Сам скрипт берём здесь. Не забываем поправить префикс, который будем добавлять к символам из библиотеки, и путь к заголовочному файлу, который будем генерировать. В нашем случае это:

header="${SRCROOT}/${PROJECT_NAME}/ext/NamespacedDependencies.h"
prefix="MYSDK"

Запускаем сборку нашего таргета `ext`. Получившаяся в результате этого библиотека нам не понадобится. Интерес представляет получившийся заголовочный файл NamespacedDependencies.h. Убедитесь, что он не пустой и содержит макросы, которые подменяют имена символов нашей библиотеки зависимостей. После этого добавьте получившийся заголовочный файл к нашему основному таргету MySDK.

NamespacedDependencies.h должен быть импортирован перед импортом любого из хедеров зависимостей. Наиболее подходящим для этого местом в проекте является PCH файл. Если он ещё не существует в проекте, то необходимо его создать. Не забываем указать путь к нему в настройках таргета.

Теперь при сборке нашего фреймворка все символы из библиотек-зависимостей будут снабжены указанным префиксом. Этот факт можно проверить запустив утилиту nm.

nm -a /Users/mik/Library/Developer/Xcode/DerivedData/Build/Products/Release-iphoneuniversal/MySDK.framework/MySDK | grep MYSDK
00000000000022be t +[MYSDK_AFURLConnectionOperation batchOfRequestOperations:progressBlock:completionBlock:]
0000000000000000 t +[MYSDK_AFURLConnectionOperation networkRequestThreadEntryPoint:]
...

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

Про ресурсы


Помимо, собственно, скомпилированной библиотеки, очень часто появляется необходимость распространять её вместе с ресурсами: такими как изображения, звуки, xib файлы и т.д. И cтруктура фреймворка может содержать в себе папку с ресурсами. Однако, Xcode её попросту игнорирует.
Чтобы иметь возможность добавить ресурсы к себе в проект, нужно распространять вместе с фреймворком либо отдельный бандл с ресурсами, либо символическую ссылку на ресурсы, расположенные внутри самого фреймворка.
Способ 1

Для ресурсов создадим отдельный бандл, который разместим внутри фреймворка.

Все наши ресурсы разместим в нём, а его, в свою очередь, уже внутри фреймворка.
Из-за того, что мы воспользовались шаблоном Bundle для OS X, важно не забыть произвести ещё несколько настроек в таргете бандла.

  1. установить Base SDK в Latest iOS (по умолчанию Latest OS X)
  2. удалить секцию Compile Sources из Build Phases
  3. установить настройку Combine High Resolution Artwork в NO. Иначе ресурсы для разных плотностей экрана будут собираться в один TIFF файл

После этой настройки можно добавить MySDKResources.bundle в секцию Copy Bundle Resources нашего основного таргета MySDK и таргет MySDKResources в Target Dependencies.
Остаётся добавить символическую ссылку на бандл, находящийся внутри фреймворка. Для этого дописываем в скрипт, собирающий наш универсальный фреймворк строчки.
pushd ${UNIVERSAL_LIBRARY_DIR}
ln -sfh "${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}Resources.bundle" "${FRAMEWORK_NAME}Resources.bundle"
popd

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

Способ 2

Существенным минусом первого способа является то, что наш фреймворк перестаёт быть цельным. А это очень существенно при распространении sdk. Для случаев, когда это применимо, можно воспользоваться таким приёмом, как встраивание ресурсов в код sdk. Такой метод удобен для хранения ресурсов небольшого размера. Например, это могут быть звуки, изображения кнопок, логотипов или текстовые ресурсы. Естественно, список не ограничивается этим набором, ресурсы могут быть абсолютно любые.
Здесь на помощь придёт утилита xxd, у которой есть замечательная опция вывода в формате статического C-массива. Я использую такой дополнительный скрипт генерации ресурсов в Build Phases проекта, который нужно добавить как Run Script перед Compile Sources.
RESOURCES_FILE="${SRCROOT}/${PROJECT_NAME}/MySDKResources.h"
rm -f ${RESOURCES_FILE}
pushd "${SRCROOT}/${PROJECT_NAME}Resources/images"
for filename in *.png; do
xxd -i $filename >> "${RESOURCES_FILE}"
done
popd

Полученный MySDKResources.h добавляем в проект. После чего ресурсами можно пользоваться, создавая объект NSData, а из него, в свою очередь, и любой другой объект, например, UIImage для изображений.

NSData *pngData = [NSData dataWithBytesNoCopy:close_png length:close_png_len freeWhenDone:NO];

Про версионирование


При распространении sdk очень удобно для каждого билда иметь уникальный номер версии. Удобство, в основном, заметно при обращении в службу поддержки или при учёте ошибок в багтрекере. Способов формирования номера версии существует множество. Я остановился на таком. Используем формат MAJOR.MINOR.BUILD, где MAJOR.MINOR задаются вручную в зависимости от политики релизов, обратной совместимости версий и т.д. В качестве BUILD берётся количество коммитов в текущей ветке репозитория (релизные сборки происходят всегда из одной ветки). Для автоматизации этого процесса используется небольшой скрипт, который добавляется в BuildPhases таргета до фазы сборки фреймворка. После первой генерации этого файла не забываем добавить его в проект с пометкой public и в наш umbrella-header:
VERSION=`git rev-list HEAD --count`
echo "#define MYSDK_VERSION @\"1.0.${VERSION}\"" > ${SRCROOT}/MYSDKVersion.h

После этого номер версии может быть использован самим sdk. Например, в качестве параметров запроса к API или для вывода в лог.

Вместо заключения


Итак, в этой статье нам удалось охватить некоторые важные аспекты разработки sdk, связанные, в основном, со сборкой. Но на этом круг вопросов, связанных с sdk, далеко не исчерпан. Незатронутыми остались такие интересные вещи, как автоматизация сборки sdk, документирование, автоматизация тестирования sdk, поддержка старых версий iOS, разработка плагинов для таких инструментов как Unity и Adobe Air. Но это — уже тема для отдельной и, может быть, не одной статьи.