Честно говоря, когда мы приступили к работе над перезапуском Яндекс.Карт, я и представить себе не мог, сколько проблем нам в итоге доставит Swift. Если вы начали писать на Swift совсем недавно, то, поверьте, вы пропустили все самое интересное. Каких-то два года назад не было инкрементной компиляции в принципе (даже пробел в любом файле приводил к полной пересборке), сам компилятор постоянно вылетал с Segmentation Fault на вполне безобидных вещах вроде тройной вложенности типов или наследования от Generic, индексация проекта длилась невообразимо долго, автодополнение работало через раз и так далее и тому подобное, всех бед не счесть. Подобные моменты несомненно усложняют жизнь программистам, но постепенно решаются с каждым обновлением Xcode. Однако есть более существенные проблемы, влияющие не только на разработку, но и на качество приложения: запрет компиляции статических библиотек из свифтового кода, а также отсутствие поддержки Swift на уровне iOS.
Изначально не было очевидно, что использование Swift и динамических библиотек приводит к росту времени запуска. Мы не сравнивали время запуска с предыдущей версией и воспринимали долгую загрузку как данность. Да и средств диагностики того, что же на самом деле происходит на этапе загрузки приложения, в общем-то не было. Но в один прекрасный день разработчики Apple добавили возможность профилирования работы системного загрузчика. Оказалось, что загрузка динамических библиотек занимает очень много времени по сравнению с другими этапами. Конечно, с нашим кодом тоже не все было идеально, но, пожалуй, это частные особенности отдельного приложения и не всем будет интересно о них читать. А вот борьба с динамическими библиотеками — общая тема для всех разработчиков, использующих Swift. Именно об этой проблеме и пойдет речь.
Загрузка приложения выполняется в два этапа. До запуска main системный загрузчик выполняет работу по подготовке образа приложения в памяти:
1. загружает динамические библиотеки,
2. проставляет адреса внешним указателям (bind) и базовые адреса внутренним указателям (rebase),
3. создает контекст Objective-C,
4. вызывает конструкторы глобальных переменных C++ и методы +load в классах Objective-C.
Только после этого начинает выполняться код приложения.
Замер pre-main — нетривиальная задача, поскольку этот этап выполняется системой и его нельзя залогировать как пользовательский код. К счастью, на WWDC 2016: Optimizing App Startup Time рассказали о переменной окружения DYLD_PRINT_STATISTICS, при включении которой в лог выводится статистика работы загрузчика по этапам. Например, для пустого приложения на Swift при запуске на iPhone 5 статистика следующая:
Загрузка системных библиотек оптимизирована — в этом просто убедиться, создав пустой проект на Objective-C и запустив его с DYLD_PRINT_LIBRARIES & DYLD_PRINT_STATISTICS:
Прежде чем начинать работу над уменьшением числа динамических библиотек, нужно определить их минимальный набор в приложении, использующем Swift. Очевидно, что это будут динамические библиотеки, линкующиеся к пустому проекту. Чтобы их посмотреть, нужно после сборки перейти к собранному бандлу (через «Show in Finder» в контекстном меню) и зайти в папку Frameworks:
Это так называемые Swift standard libraries (swift runtime). Если в проект добавлен хотя бы один файл *.swift, Xcode копирует их в бандл и линкует к бинарному файлу. Зачем они нужны? Все дело в молодости языка. Swift продолжает активно развиваться и не поддерживает бинарную совместимость. Если бы swift runtime сделали частью системы (как это уже давно сделано для Objective-C), то при очередном обновлении iOS старые программы не смогли бы работать на новой версии системы и наоборот. Поэтому приложения содержат копию swift runtime в папке Frameworks, причем система рассматривает их как пользовательские, отсюда и долгая загрузка. Такова плата за использование динамично развивающегося языка.
Перейдем к более сложному примеру. Пусть некоторое приложение:
— использует СocoaPods для подключения зависимостей, причем некоторые зависимости приходят готовыми динамическими библиотеками,
— разбито на несколько таргетов,
— использует CoreLocation, MapKit, AVFoundation.
Внутри его бандла, в папке Frameworks, лежат следующие библиотеки:
Статистика загрузки этого приложения на iPhone 5 выглядит так:
Как видно, в указанном примере на пять библиотек swift runtime больше, чем в пустом проекте. Если в каком-либо файле *.swift есть import CoreLocation, или #import <CoreLocation/CoreLocation.h> стоит в bridging header, то Xcode добавляет в бандл libswiftCoreLocation.dylib. При этом использование #import <CoreLocation/CoreLocation.h> в коде на Objective-C не приводит к добавлению этой библиотеки. Напрашивается решение — сделать обертки Objective-C над нужными частями CoreLocation и использовать в приложении только их. Пример оберток можно посмотреть тут.
К сожалению, этого может оказаться недостаточно из-за транзитивных зависимостей. Использование import MapKit в любом файле *.swift приводит к добавлению libswiftmapkit.dylib и libswiftCoreLocation.dylib, использование import AVFoundation — к добавлению libswiftAVFoundation.dylib, libswiftCoreAudio.dylib, libswiftCoreMedia.dylib. Поэтому нужные части MapKit и AVFoundation тоже приходится оборачивать. А еще libswiftCoreLocation.dylib добавляется, если есть #import <CoreLocation/CoreLocation.h> в каком-либо заголовочном файле, от которого транзитивно зависит bridging header. Если этот #import находится в какой-либо библиотеке, то ее тоже нужно будет обернуть. Все это звучит неприятно, но результат оправдан — можно достигнуть того же набора Swift standard libraries, что и в пустом приложении.
Следующий массовый источник динамических библиотек — поды, собираемые в динамические фреймворки при указании !use_frameworks в Podfile. Флаг !use_frameworks необходим для подключения зависимостей, написанных на Swift, поскольку Xcode не разрешает использование Swift в статических фреймворках — выкидывает ошибку «Swift is not supported for static libraries».
На самом деле это не значит, что нельзя создавать и использовать статические библиотеки с кодом на Swift, так как статическая библиотека — это просто архив объектных файлов. Компилятор Swift для каждого исходного файла генерирует обычные объектники формата Mach-O. При помощи ar или libtool их можно заархивировать в статическую библиотеку и подставить результат в команду линковки:
— Пусть модуль SomeLib состоит из двух файлов: SomeClass.swift и SomeOtherClass.swift. SomeLib можно собрать с Xcode 8.3.1 в статическую библиотеку и слинковать с main.swift следующими командами:
К счастью, в Xcode 9 beta 4 запрет на использование Swift в статических фреймворках убран. Можно дождаться релиза Xcode и соответствующих правок в cocoapods (чтобы компилировались статические фреймворки), и проблема исчезнет сама собой. Но для тех, кто не планирует или не может перейти на Xcode 9, стоит упомянуть про имеющееся достаточно простое решение — cocoapods-amimono. Идея проста — после сборки каждого пода в отдельной билд-папке остаются билд-артефакты, в том числе объектные файлы. Вместо линковки с динамическими библиотеками можно слинковаться напрямую с объектными файлами, из которых они были собраны. Cocoapods-amimono:
— добавляет билд-фазу, выполняющую скрипт, который составляет LinkFileList из объектных файлов, находящихся в build-папках подов,
— линковку с фреймворками подов заменяет на линковку с LinkFileList,
— удаляет встраивание фреймворков в бандл приложения.
Решение работает: фреймворки подов исчезают из папки Frameworks, при этом можно использовать module import, то есть код приложения не меняется.
Таким же образом можно избавиться и от динамических фреймворков, собираемых из пользовательских таргетов: либо дождаться Xcode 9 (и использовать статические фреймворки), либо линковать объектники напрямую в бинарный файл приложения, как это делает cocoapods-amimono. Для этого нужно:
— оставить target в dependencies основного таргета,
— не встраивать framework в бандл и не линковаться с ним,
— добавить билд-фазу, составляющую LinkFileList, по аналогии с cocoapods-amimono:
С готовыми динамическими фреймворками сложнее, так как динамическую библиотеку нельзя преобразовать в статическую — это по сути исполняемый файл, допускающий только динамическую линковку. Если это core-framework приложения и его символы нужны сразу при запуске — ничего не сделать, его использование будет неизбежно увеличивать время запуска. Но если фреймворк не используется при старте программы, то можно загружать его лениво через dlopen. Причем лениво загружать через dlopen+dlsym можно только совместимую с Objective-C часть интерфейса, поскольку при module import в Swift библиотека линкуется автоматически. Если все необходимое доступно из Objective-C, то нужно:
1. Убрать линковку библиотеки с основным таргетом. Если зависимость подключается через cocoapods, то убрать линковку можно через добавление фальшивого таргета (к которому будут привязаны проблемные поды) или через post_install в Podfile:
— Загрузка библиотеки.
В итоге остаются только библиотеки swift runtime и vendored-фреймворки, загружаемые по-возможности лениво. Причем набор библиотек swift runtime такой же, как у пустого приложения. Статистика pre-main теперь выглядит так:
Есть пара простых предложений, как не испортить достигнутый результат с очередным обновлением. Первое — добавить в билд-фазы выполнение скрипта, проверяющего после сборки список библиотек и фреймворков в папке Frameworks бандла приложения — не появилось ли что-то новое.
Перечисленные проблемы целиком и полностью порождены молодостью Swift. Часть из них исчезнет с выходом Xcode 9, в котором разрешены статические библиотеки на Swift, что позволит избавиться от костылей вроде cocoapods-amimono. Но окончательно проблема роста размера бандла и времени запуска приложения решится только тогда, когда swift runtime станет частью iOS. Причем еще какое-то время после этого приложениям придется таскать его с собой, чтобы поддерживать предыдущие версии системы. Разработка Swift 5 нацелена на стабилизацию бинарного интерфейса Swift standard library. Бинарный интерфейс планировалось стабилизировать в Swift 4, но Xcode 9 по-прежнему копирует swift runtime в бандл приложения с deployment target iOS 11, а значит, Swift все еще не является частью iOS.
Изначально не было очевидно, что использование Swift и динамических библиотек приводит к росту времени запуска. Мы не сравнивали время запуска с предыдущей версией и воспринимали долгую загрузку как данность. Да и средств диагностики того, что же на самом деле происходит на этапе загрузки приложения, в общем-то не было. Но в один прекрасный день разработчики Apple добавили возможность профилирования работы системного загрузчика. Оказалось, что загрузка динамических библиотек занимает очень много времени по сравнению с другими этапами. Конечно, с нашим кодом тоже не все было идеально, но, пожалуй, это частные особенности отдельного приложения и не всем будет интересно о них читать. А вот борьба с динамическими библиотеками — общая тема для всех разработчиков, использующих Swift. Именно об этой проблеме и пойдет речь.
Pre-main
Загрузка приложения выполняется в два этапа. До запуска main системный загрузчик выполняет работу по подготовке образа приложения в памяти:
1. загружает динамические библиотеки,
2. проставляет адреса внешним указателям (bind) и базовые адреса внутренним указателям (rebase),
3. создает контекст Objective-C,
4. вызывает конструкторы глобальных переменных C++ и методы +load в классах Objective-C.
Только после этого начинает выполняться код приложения.
Замер pre-main — нетривиальная задача, поскольку этот этап выполняется системой и его нельзя залогировать как пользовательский код. К счастью, на WWDC 2016: Optimizing App Startup Time рассказали о переменной окружения DYLD_PRINT_STATISTICS, при включении которой в лог выводится статистика работы загрузчика по этапам. Например, для пустого приложения на Swift при запуске на iPhone 5 статистика следующая:
Total pre-main time: 1.0 seconds (100.0%)
dylib loading time: 975.17 milliseconds (95.8%)
rebase/binding time: 14.39 milliseconds (1.4%)
ObjC setup time: 12.46 milliseconds (1.2%)
initializer time: 15.27 milliseconds (1.6%)
Видно, что огромную часть pre-main занимает загрузка динамических библиотек. Их список можно получить, воспользовавшись переменной окружения DYLD_PRINT_LIBRARIES. Библиотеки делятся на системные и пользовательские, загружаемые из папки Frameworks бандла приложения.Загрузка системных библиотек оптимизирована — в этом просто убедиться, создав пустой проект на Objective-C и запустив его с DYLD_PRINT_LIBRARIES & DYLD_PRINT_STATISTICS:
dyld: loaded: /var/containers/Bundle/Application/6232DEDA-1E38-44B9-8CE8-01E244711306/Test.app/Test
...
dyld: loaded: /System/Library/Frameworks/JavaScriptCore.framework/JavaScriptCore
dyld: loaded: /System/Library/Frameworks/AudioToolbox.framework/AudioToolbox
dyld: loaded: /System/Library/PrivateFrameworks/TCC.framework/TCC
Total pre-main time: 19.65 milliseconds (100.0%)
dylib loading time: 1.32 milliseconds (6.7%)
rebase/binding time: 1.30 milliseconds (6.6%)
ObjC setup time: 5.11 milliseconds (26.0%)
initializer time: 11.90 milliseconds (60.5%)
Этап загрузки динамических библиотек выполняется практически мгновенно, хотя на самом деле их 147, и все — системные. Поэтому сфокусироваться нужно на пользовательских библиотеках. Минимальный набор динамических библиотек
Прежде чем начинать работу над уменьшением числа динамических библиотек, нужно определить их минимальный набор в приложении, использующем Swift. Очевидно, что это будут динамические библиотеки, линкующиеся к пустому проекту. Чтобы их посмотреть, нужно после сборки перейти к собранному бандлу (через «Show in Finder» в контекстном меню) и зайти в папку Frameworks:
Это так называемые Swift standard libraries (swift runtime). Если в проект добавлен хотя бы один файл *.swift, Xcode копирует их в бандл и линкует к бинарному файлу. Зачем они нужны? Все дело в молодости языка. Swift продолжает активно развиваться и не поддерживает бинарную совместимость. Если бы swift runtime сделали частью системы (как это уже давно сделано для Objective-C), то при очередном обновлении iOS старые программы не смогли бы работать на новой версии системы и наоборот. Поэтому приложения содержат копию swift runtime в папке Frameworks, причем система рассматривает их как пользовательские, отсюда и долгая загрузка. Такова плата за использование динамично развивающегося языка.
Борьба с динамическими библиотеками
Перейдем к более сложному примеру. Пусть некоторое приложение:
— использует СocoaPods для подключения зависимостей, причем некоторые зависимости приходят готовыми динамическими библиотеками,
— разбито на несколько таргетов,
— использует CoreLocation, MapKit, AVFoundation.
Внутри его бандла, в папке Frameworks, лежат следующие библиотеки:
Статистика загрузки этого приложения на iPhone 5 выглядит так:
Total pre-main time: 3.6 seconds (100.0%)
dylib loading time: 3.5 seconds (95.3%)
rebase/binding time: 50.04 milliseconds (1.3%)
ObjC setup time: 59.78 milliseconds (1.6%)
initializer time: 60.02 milliseconds (1.8%)
Уменьшение числа Swift standard libraries
Как видно, в указанном примере на пять библиотек swift runtime больше, чем в пустом проекте. Если в каком-либо файле *.swift есть import CoreLocation, или #import <CoreLocation/CoreLocation.h> стоит в bridging header, то Xcode добавляет в бандл libswiftCoreLocation.dylib. При этом использование #import <CoreLocation/CoreLocation.h> в коде на Objective-C не приводит к добавлению этой библиотеки. Напрашивается решение — сделать обертки Objective-C над нужными частями CoreLocation и использовать в приложении только их. Пример оберток можно посмотреть тут.
К сожалению, этого может оказаться недостаточно из-за транзитивных зависимостей. Использование import MapKit в любом файле *.swift приводит к добавлению libswiftmapkit.dylib и libswiftCoreLocation.dylib, использование import AVFoundation — к добавлению libswiftAVFoundation.dylib, libswiftCoreAudio.dylib, libswiftCoreMedia.dylib. Поэтому нужные части MapKit и AVFoundation тоже приходится оборачивать. А еще libswiftCoreLocation.dylib добавляется, если есть #import <CoreLocation/CoreLocation.h> в каком-либо заголовочном файле, от которого транзитивно зависит bridging header. Если этот #import находится в какой-либо библиотеке, то ее тоже нужно будет обернуть. Все это звучит неприятно, но результат оправдан — можно достигнуть того же набора Swift standard libraries, что и в пустом приложении.
Статическая линковка подов, поставляемых исходными файлами
Следующий массовый источник динамических библиотек — поды, собираемые в динамические фреймворки при указании !use_frameworks в Podfile. Флаг !use_frameworks необходим для подключения зависимостей, написанных на Swift, поскольку Xcode не разрешает использование Swift в статических фреймворках — выкидывает ошибку «Swift is not supported for static libraries».
На самом деле это не значит, что нельзя создавать и использовать статические библиотеки с кодом на Swift, так как статическая библиотека — это просто архив объектных файлов. Компилятор Swift для каждого исходного файла генерирует обычные объектники формата Mach-O. При помощи ar или libtool их можно заархивировать в статическую библиотеку и подставить результат в команду линковки:
— Пусть модуль SomeLib состоит из двух файлов: SomeClass.swift и SomeOtherClass.swift. SomeLib можно собрать с Xcode 8.3.1 в статическую библиотеку и слинковать с main.swift следующими командами:
DEVELOPER_DIR=/Applications/Xcode8.3.1.app/Contents/Developer/
SWIFTC=$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc
SDK=$DEVELOPER_DIR/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.3.sdk
# сгенерировать объектные файлы
$SWIFTC -target armv7-apple-ios8.0 -sdk $SDK -module-name SomeLib \
SomeClass.swift SomeOtherClass.swift -c
# сгенерировать swiftmodule, необходимый для импорта в main.swift
$SWIFTC -target armv7-apple-ios8.0 -sdk $SDK -module-name SomeLib \
SomeClass.swift SomeOtherClass.swift -emit-module
# создать статическую библиотеку из объектных файлов
$libtool -static -o libSomeLib.a SomeClass.o SomeOtherClass.o
# создать исполняемый файл из main.swift и libSomeLib.a
$SWIFTC -target armv7-apple-ios8.0 -sdk $SDK -I . -L . main.swift -lSomeLib
— Первые две команды можно объединить, используя OutputFileMap.json, как это делает Xcode. Конкретные параметры, с которыми драйвер компиляции swiftc вызывает компилятор swift, можно посмотреть, добавив опцию -v.К счастью, в Xcode 9 beta 4 запрет на использование Swift в статических фреймворках убран. Можно дождаться релиза Xcode и соответствующих правок в cocoapods (чтобы компилировались статические фреймворки), и проблема исчезнет сама собой. Но для тех, кто не планирует или не может перейти на Xcode 9, стоит упомянуть про имеющееся достаточно простое решение — cocoapods-amimono. Идея проста — после сборки каждого пода в отдельной билд-папке остаются билд-артефакты, в том числе объектные файлы. Вместо линковки с динамическими библиотеками можно слинковаться напрямую с объектными файлами, из которых они были собраны. Cocoapods-amimono:
— добавляет билд-фазу, выполняющую скрипт, который составляет LinkFileList из объектных файлов, находящихся в build-папках подов,
— линковку с фреймворками подов заменяет на линковку с LinkFileList,
— удаляет встраивание фреймворков в бандл приложения.
Решение работает: фреймворки подов исчезают из папки Frameworks, при этом можно использовать module import, то есть код приложения не меняется.
Статическая линковка собственных таргетов
Таким же образом можно избавиться и от динамических фреймворков, собираемых из пользовательских таргетов: либо дождаться Xcode 9 (и использовать статические фреймворки), либо линковать объектники напрямую в бинарный файл приложения, как это делает cocoapods-amimono. Для этого нужно:
— оставить target в dependencies основного таргета,
— не встраивать framework в бандл и не линковаться с ним,
— добавить билд-фазу, составляющую LinkFileList, по аналогии с cocoapods-amimono:
# таргеты, которые нужно статически влинковать
DEPENDENCIES=('SomeTarget' 'SomeOtherTarget');
ARCHS_LIST=($ARCHS)
# итерация по архитектурам, для которых проводится сборка
for ARCH in ${ARCHS[@]}; do
DIR=$OBJECT_FILE_DIR_normal/$ARCH
# абсолютный путь до создаваемого LinkFileList
FILE_PATH=$DIR/$TARGET_NAME.Dependencies.LinkFileList
FILE_LIST=""
# итерация по таргетам
for DEPENDENCY in "${DEPENDENCIES[@]}"; do
# путь до папки, содержащей билд-артефакты таргета
PATH=$CONFIGURATION_TEMP_DIR/${DEPENDENCY}.build/Objects-normal/$ARCH
# паттерн объектных файлов
SEARCH_EXP="$PATH/*.o"
# итерация по всем файлам, удовлетворяющим SEARCH_EXP
for OBJ_FILE in $SEARCH_EXP; do
# добавить файл в FILE_LIST
FILE_LIST+="${OBJ_FILE}\n"
done
done
FILE_LIST=${FILE_LIST%$'\n'}
# записать FILE_LIST на диск по пути FILE_PATH
echo -n -e $FILE_LIST > $FILE_PATH
done
— линковать основной таргет с LinkFileList. Для этого в OTHER_LDFLAGS добавить:-filelist "${OBJECT_FILE_DIR_normal}/${CURRENT_ARCH}/${TARGET_NAME}.Dependencies.LinkFileList"
Ленивая загрузка динамических библиотек
С готовыми динамическими фреймворками сложнее, так как динамическую библиотеку нельзя преобразовать в статическую — это по сути исполняемый файл, допускающий только динамическую линковку. Если это core-framework приложения и его символы нужны сразу при запуске — ничего не сделать, его использование будет неизбежно увеличивать время запуска. Но если фреймворк не используется при старте программы, то можно загружать его лениво через dlopen. Причем лениво загружать через dlopen+dlsym можно только совместимую с Objective-C часть интерфейса, поскольку при module import в Swift библиотека линкуется автоматически. Если все необходимое доступно из Objective-C, то нужно:
1. Убрать линковку библиотеки с основным таргетом. Если зависимость подключается через cocoapods, то убрать линковку можно через добавление фальшивого таргета (к которому будут привязаны проблемные поды) или через post_install в Podfile:
post_install do | installer |
# вызвать Amimono::Patcher.patch!(installer), если используется amimono
# итерация по таргетам, агрегирующим зависимости основных таргетов
installer.aggregate_targets.each do |aggregate_target|
# xcconfig-и, с которыми собираются агрегируюшие и основные таргеты
target_xcconfigs = aggregate_target.xcconfigs
# у каждой конфигурации - свой xcconfig
aggregate_target.user_build_configurations.each do |config_name,_|
# путь до xcconfig для конкретной конфигурации
path = aggregate_target.xcconfig_path(config_name)
# взять текущее состояние
xcconfig = Xcodeproj::Config.new(path)
# удалить что нужно
xcconfig.frameworks.delete("SomeFramework")
# перезаписать
xcconfig.save_as(path)
end
end
end
2. Написать на Objective-C обертку над framework-ом, реализующую ленивую загрузку библиотеки и нужных символов.— Загрузка библиотеки.
#import <dlfcn.h>
NSString *frameworksPath = [[NSBundle mainBundle] privateFrameworksPath];
NSString *dyLib = @"DynamicLib.framework/DynamicLib";
// абсолютный путь до файла библиотеки
NSString *path = [NSString stringWithFormat:@"%@/%@", frameworksPath, dyLib];
const char *pathPtr = [path cStringUsingEncoding:NSASCIIStringEncoding]
// загрузка библиотеки
void *handle = dlopen(pathPtr, RTLD_LAZY);
— Получение имен символов в библиотеке DynamicLib, по которым их далее нужно загружать через dlsym. $nm -gU $BUNDLE_PATH/Frameworks/DynamicLib.framework/DynamicLib
DynamicLib (for architecture armv7):
00007ef0 S _DynamicLibVersionNumber
00007ec8 S _DynamicLibVersionString
0000837c S _OBJC_CLASS_$__TtC10DynamicLib16SomeClass
00008408 D _OBJC_METACLASS_$__TtC10DynamicLib16SomeClass
...
00004b98 T _someGlobalFunc
000083f8 D _someGlobalStringVar
000083f4 D _someGlobalVar
...
— Загрузка и использование глобальных символов.// dlsym возвращает указатель на символ библиотеки.
// получение указателя на функцию
int (*someGlobalFuncPtr)(int) = dlsym(handle, "someGlobalFunc");
// вызов функции по ее указателю
someGlobalFuncPtr(5);
// получение указателя на глобальную переменную
int *someGlobalVarPtr = (int *)dlsym(handle, "someGlobalVar");
NSLog(@"%@", *someGlobalVarPtr);
// использование глобальной переменной через разыменование указателя
NSString *__autoreleasing *someGlobalStringVarPtr =
(NSString *__autoreleasing *)dlsym(handle, "someGlobalStringVar");
NSLog(@"%@", *someGlobalStringVarPtr);
*someGlobalStringVar = @"newValue";
— Загрузка и использование классов. Objective-C позволяет вызвать у объекта типа id любой объявленный в каком-либо классе instance-метод, а у объекта типа Class — любой объявленный class-метод. Причем можно использовать заголовочные файлы с объявлением интерфейса нужного класса, это не вызывает автоматической загрузки библиотеки, как в случае со Swift.#import <DynamicLib/SomeClass.h>
//dlsym возвращает сущность типа Class
Class class = (__bridge Class)dlsym(handle,
"OBJC_CLASS_$__TtC10DynamicLib16SomeClass")
// вызов class-метода
[class someClassFunc];
// создание объекта
SomeClass *obj = [(SomeСlass *)[class alloc] init];
// использование
NSLog(@"%@", obj.someVar)
[obj someMethod];
Пример целиком можно посмотреть тут. Стандартные действия по загрузке библиотеки и символов можно оформить в макросы, как это сделано в Facebook SDK.Результат оптимизаций
В итоге остаются только библиотеки swift runtime и vendored-фреймворки, загружаемые по-возможности лениво. Причем набор библиотек swift runtime такой же, как у пустого приложения. Статистика pre-main теперь выглядит так:
Total pre-main time: 1.0 seconds (100.0%)
dylib loading time: 963.68 milliseconds (90.0%)
rebase/binding time: 35.65 milliseconds (3.3%)
ObjC setup time: 29.08 milliseconds (2.7%)
initializer time: 41.35 milliseconds (4.0%)
Время загрузки динамических библиотек сократилось c 3,5 до 1 секунды.Сохранение результата
Есть пара простых предложений, как не испортить достигнутый результат с очередным обновлением. Первое — добавить в билд-фазы выполнение скрипта, проверяющего после сборки список библиотек и фреймворков в папке Frameworks бандла приложения — не появилось ли что-то новое.
FRAMEWORKS_DIR="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}"
FRAMEWORKS_SEARCH_PATTERN="${FRAMEWORKS_DIR}/*"
# возвращает все элементы на диске, удовлетворяющие FRAMEWORKS_SEARCH_PATTERN
FRAMEWORKS=($FRAMEWORKS_SEARCH_PATTERN)
# ожидаемый список файлов и папок в Frameworks
ALLOWED_FRAMEWORKS=(libswiftFoundation.dylib SomeFramework.framework)
for FRAMEWORK in ${ALLOWED_FRAMEWORKS[@]}
do
PATTERN="*${FRAMEWORK}"
# удалить все элементы, удовлетворяющие PATTERN
FRAMEWORKS=(${FRAMEWORKS[@]/${PATTERN}/})
done
echo ${FRAMEWORKS[@]}
# вернуть число оставшихся элементов в FRAMEWORKS
# любое ненулевое число будет интерпретировано как ошибка сборки
exit ${#FRAMEWORKS[@]}
Если появились какие-то новые файлы, это точно повод для разбирательств. Но могут быть библиотеки, которые должны загружаться лениво, и важно проверять, что они не начали загружаться на старте. Поэтому второе предложение — получать список загруженных библиотек через objc_copyImageNames и проверять список библиотек, загруженных из Frameworks:var count: UInt32 = 0
// получение списка загруженных библиотек
let imagesPathsPointer: UnsafeMutablePointer<UnsafePointer?>! =
objc_copyImageNames(&count)
// ожидаемый список загруженных библиотек
let expectedImages: Set = ["libswiftCore.dylib"]
// путь до папки с библиотеками внутри бандла приложения
let frameworksPath = Bundle.main.privateFrameworksPath ?? "none"
for i in 0..<count {
let pathPointer = imagesPathsPointer.advanced(by: Int(i)).pointee
let path = pathPointer.flatMap { String(cString: $0) } ?? ""
// системные библиотеки не учитываем
guard path.contains(frameworksPath) else { continue }
let name = (path as NSString).lastPathComponent
assert(expectedImages.contains(name))
}
Список не должен меняться. Этих двух моментов вполне достаточно, чтобы увеличение времени pre-main за счет увеличения времени загрузки динамических библиотек не прошло незаметным.Заключение
Перечисленные проблемы целиком и полностью порождены молодостью Swift. Часть из них исчезнет с выходом Xcode 9, в котором разрешены статические библиотеки на Swift, что позволит избавиться от костылей вроде cocoapods-amimono. Но окончательно проблема роста размера бандла и времени запуска приложения решится только тогда, когда swift runtime станет частью iOS. Причем еще какое-то время после этого приложениям придется таскать его с собой, чтобы поддерживать предыдущие версии системы. Разработка Swift 5 нацелена на стабилизацию бинарного интерфейса Swift standard library. Бинарный интерфейс планировалось стабилизировать в Swift 4, но Xcode 9 по-прежнему копирует swift runtime в бандл приложения с deployment target iOS 11, а значит, Swift все еще не является частью iOS.