How iOS 15 makes your app launch faster
Read the full version of this post on the Emerge Tools Blog
The most intriguing feature from WWDC21 was buried deep in the Xcode 13 release notes:
All programs and
dylibsbuilt with a deployment target of macOS 12 or iOS 15 or later now use the chained fixups format. This uses different load commands and LINKEDIT data, and won’t run or load on older OS versions.
There isn’t any documentation or sessions to learn more about this change, but we can reverse engineer it to see what Apple is doing differently on the new OSes and if it will help your apps. First, a bit of background on the program that controls app startup.
The dynamic linker (dyld) is the entry point of every app. It’s responsible for getting your code ready to run, so it would make sense that any improvement to dyld would result in improved app launch time. Before calling main, running static initializers, or setting up the Objective-C runtime, dyld performs fixups. These consist of rebase and bind operations which modify pointers in the app binary to contain addresses that will be valid at runtime. To see what these look like, you can use the
dyldinfo command line tool.
% xcrun dyldinfo -rebase -bind Snapchat.app/Snapchat
rebase information (from compressed dyld info):
segment section address type
__DATA __got 0x10748C0C8 pointer
segment section address type addend dylib symbol
__DATA __const 0x107595A70 pointer 0 libswiftCore _$sSHMp
This means address
0x10748C0C8 is located in
__DATA/__got and needs to be shifted by a constant value (known as the slide). While address
0x107595A70 is in
__DATA/__const and should point to the protocol descriptor for Hashable found in
dyld uses the
LC_DYLD_INFO load command and
dyld_info_command struct to determine the location and size of rebases, binds and exported symbols in a binary. Emerge (disclaimer: I’m the founder 😬), parses this data to let you visualize their contribution to binary size as well as suggest linker flags to make them smaller:
A new format
When I first uploaded an app built for iOS 15 to Emerge there was no visualization of dyld fixups. This was because the
LC_DYLD_INFO_ONLY load command was missing, it had been replaced by
% otool -l iOS14Example.app/iOS14Example | grep LC_DYLD
cmd LC_DYLD_INFO_ONLY% otool -l iOS15Example.app/iOS15Example | grep LC_DYLD
The export data is exactly the same as before, a trie where each node represents part of a symbol name.
The only change in iOS 15 is the data is now referenced by a
linkedit_data_command which contains the offset of the first node. To validate this, I wrote a short Swift app to parse the iOS 15 binary and print each symbol:
The real change is in
LC_DYLD_CHAINED_FIXUPS. Before iOS 15, rebases, binds and lazy binds were each stored in a separate table. Now they have been combined into chains, with pointers to the starts of the chains contained in this new load command…