Самая интригующая часть нового функционала, появившегося по итогам проведения конференции WWDC21, была запрятана в замечаниях к выпуску Xcode 13.
Все программы и библиотеки dylib
, имеющие в качестве целевой платформы развертывания macOS 12 или iOS 15 и более поздние их версии, теперь используют формат объединенных в цепочки адресных привязок. При этом задействуются различные команды загрузки и данные LINKEDIT, а сами эти программы и библиотеки не будут запускаться или загружаться на более ранних версиях ОС.
Этому нововведению на конференции не было посвящено никаких тематических секций, и какая-либо документация по нему отсутствует. Но мы попробуем раскрыть его нюансы и разобраться в том, что в Apple теперь делают по-другому на новых операционных системах и какую пользу это несет приложениям. Первым делом расскажем немного о программе, которая управляет запуском приложения.
dyld
Динамический компоновщик (dyld
) представляет собой точку входа каждого приложения и обеспечивает подготовку кода к запуску. Поэтому разумно ожидать, что любое улучшение dyld
приведет к уменьшению времени запуска приложения. Прежде чем вызывается main
, запускается блок статической инициализации или настраивается среда выполнения Objective-C, dyld
выполняет адресные привязки. Они состоят из операций перемещения и привязки, которые изменяют указатели в двоичном файле приложения. Причем делают это так, чтобы они содержали адреса, которые будут действительны во время выполнения. Посмотрим, как эти операции выглядят. Для этого воспользуемся инструментом командной строки dyldinfo
.
% xcrun dyldinfo -rebase -bind Snapchat.app/Snapchat
rebase information (from compressed dyld info):
segment section address type
__DATA __got 0x10748C0C8 pointer
...
bind information:
segment section address type addend dylib symbol
__DATA __const 0x107595A70 pointer 0 libswiftCore _$sSHMp
И вот что здесь происходит. Адрес 0x10748C0C8
находится в __DATA/__got
. Его надо переместить, т. е. сдвинуть на некое постоянное значение, которое так и называется «сдвиг». А когда адрес 0x107595A70
находится уже в __DATA/__const
и должен указывать на дескриптор протокола для Hashable из libswiftCore.dylib
, dyld
использует команду загрузки LC_DYLD_INFO
и структуру dyld_info_command
для определения в двоичном файле местоположения и размера перемещений, привязок и экспортируемых символов. На этом ресурсе происходит парсинг этих данных для визуальной оценки влияния перемещений и привязок на размер двоичного кода, а также предлагаются флаги компоновщика для уменьшения этого размера:
Новый формат
Когда я впервые загрузил туда приложение для iOS 15, визуализации адресных привязок dyld
еще не было. Отсутствовавшая команда загрузки LC_DYLD_INFO_ONLY
была заменена на LC_DYLD_CHAINED_FIXUPS
и LC_DYLD_EXPORTS_TRIE
.
% otool -l iOS14Example.app/iOS14Example | grep LC_DYLD
cmd LC_DYLD_INFO_ONLY% otool -l iOS15Example.app/iOS15Example | grep LC_DYLD
cmd LC_DYLD_CHAINED_FIXUPS
cmd LC_DYLD_EXPORTS_TRIE
Экспорт данных точно такой же, как и раньше: префиксное дерево, где каждый узел является частью имени символа.
Единственное изменение в iOS 15: на данные теперь ссылаются с помощью linkedit_data_command
, в котором содержится смещение первого узла. Чтобы удостовериться в этом, пришлось написать короткое приложение на Swift для выполнения парсинга двоичного кода iOS 15 и вывода каждого символа:
let bytes = (try! Data(contentsOf: url) as NSData).bytes
bytes.processLoadComands { load_command, pointer in
if load_command.cmd == LC_DYLD_EXPORTS_TRIE {
let dataCommand = pointer.load(as: linkedit_data_command.self)
bytes.advanced(by: Int(dataCommand.dataoff)).readExportTrie()
}
}
extension UnsafeRawPointer {
func readExportTrie() {
var frontier = readNode(name: "")
guard !frontier.isEmpty else { return }
repeat {
let (prefix, offset) = frontier.removeFirst()
let children = advanced(by: Int(offset)).readNode(name: prefix)
for (suffix, offset) in children {
frontier.append((prefix + suffix, offset))
}
} while !frontier.isEmpty
}
// Возвращает массив дочерних узлов и их смещение
func readNode(name: String) -> [(String, UInt)] {
guard load(as: UInt8.self) == 0 else {
// Это конечный узел
print("symbol name \(name)")
return []
}
let numberOfBranches = UInt(advanced(by: 1).load(as: UInt8.self))
var mutablePointer = self.advanced(by: 2)
var result = [(String, UInt)]()
for _ in 0..<numberOfBranches {
result.append(
(mutablePointer.readNullTerminatedString(),
mutablePointer.readULEB()))
}
return result
}
}
Цепочки привязок
Реальное изменение произошло в LC_DYLD_CHAINED_FIXUPS
. До iOS 15 перемещения, привязки и «ленивые» привязки хранились каждая в отдельной таблице. Теперь они объединены в цепочки, причем имеются указатели на начало цепочек, содержащихся в этой новой команде загрузки.
Читайте также:
- Как проще всего выполнять запросы GraphQL в iOS
- Анимируем скучные табличные представления в iOS-приложении
- Обработка ошибок API в веб-приложении, используя Axios
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Noah Martin: How iOS 15 makes your app launch faster