Самая интригующая часть нового функционала, появившегося по итогам проведения конференции 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 перемещения, привязки и «ленивые» привязки хранились каждая в отдельной таблице. Теперь они объединены в цепочки, причем имеются указатели на начало цепочек, содержащихся в этой новой команде загрузки.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Noah Martin: How iOS 15 makes your app launch faster

Предыдущая статьяКак подключить базу данных MySQL к сайту на PHP
Следующая статьяКогда программисты выходят на пенсию? 35 — новые 55?