Discover how mergeable libraries combine the best parts of static and dynamic libraries to help improve your app's productivity and runtime performance. Learn how you can enable faster development while shipping the smallest app. We'll show you how to adopt mergeable libraries in Xcode 15 and share best practices for working with your code.
♪ ♪ Cyndy: Hello, I'm Cyndy, and I'm a compiler engineer on the Languages and Runtimes Team. In this session, we're meeting Mergeable Libraries. This is a new model for building and distributing libraries powered by the static linker. I'll share how mergeable libraries work to make your apps build and run faster.
Then, I'll demonstrate how to enable mergeable libraries in Xcode 15. And lastly, I'll cover considerations and what we recommend when using mergeable libraries. Before we dive in, I'll provide a brief overview of static and dynamic libraries. This will highlight the benefits of mergeable ones.
Static libraries are a collection of object files. At build time, the static linker finds which APIs to use from those libraries and copies that code into the app binary.
And since it's copied, the library isn't needed after building. If code in static libraries changes or if more libraries are used, it introduces a build time slowdown. This is a result of how they are archived and linked into apps, which makes iterative building and debugging slower. Instead, dynamic libraries can be used to prevent this. Dynamic libraries are commonly called dylibs. They are the binary file type for framework targets in Xcode.
The code from frameworks isn't copied into executables. Rather, the static linker records the installed path of the library into the app binary for later. Any frameworks not in the Apple SDK must be embedded into the app bundle. The key difference is when dynamic libraries are added or updated, the static linker does not need to copy code. This results in faster builds. However, it adds complexity to when apps are used at runtime. This is where a dynamic linker is needed. When an app is launched, the dynamic linker named dyld must find and load framework dependencies. This includes libraries those frameworks depend on.
As more are used, this results in a steady increase in memory consumption and app launch time. And when you factor in dependencies from the Apple SDK, apps can often load hundreds of frameworks. Our platforms have heavily optimized system libraries to account for this. But this doesn't apply to frameworks that get embedded in apps. So to recap, there are some tradeoffs when deciding between using static and dynamic libraries.
While dynamic libraries have little impact on build time but noticeable launch time consequences, static libraries provide minimal launch time impact but are costly on build time. Due to this, we have historically recommended measuring what's best for your app. With mergeable libraries, this is no longer needed. Mergeable libraries unlock the best of both linking strategies. I'll describe how mergeable libraries are able to optimize for performance and development. Consider any binary image, like an executable. The frameworks this binary depends on are given to the static linker. These dependencies can become mergeable libraries. And the linked output can become the merged binary. But what makes these dependencies mergeable? This can be explained by how they are built. Any dynamic library can be built as mergeable. When the static linker creates the library, it also generates metadata. The metadata is within the binary, increasing its overall size. It allows the linker to treat the library similarly to a static library when it's used as a link dependency. With the metadata, users of the library can choose to statically link as normal dynamic libraries or merge them. The merged binary output can be an executable, like an app, or another dynamic library, like a framework. Merging is comparable to how static libraries get linked. In the end, you're left with a binary that contains the segments of the libraries. And that output binary remains the same file type. Merging is brand-new in Xcode 15. The newly implemented static linker is what enables this. It works by using new linker options. First, the libraries to merge are built with the option -make_mergeable. This tells the linker to record the metadata. Next, for the merged binary, the linker uses that metadata along with the libraries to produce the final output with the option -merge_library or -merge_framework. Xcode handles these details for you. However, you can see these options being applied when inspecting your build log. But how is merging better than just linking? Well, let's consider the size after merging. Firstly, libraries and their metadata aren't needed and can be removed after they've been merged. So the only focus is the size of the merged binary. When merging, the linker can de-duplicate content, such as strings, across all libraries. For instance, it removes redundant symbol references, Objective-C selectors, and objc_msgsend stubs. This results in a smaller overall app bundle. The image type of the final binary remains the same too. That means any already supported linker optimizations can be applied.
This also has a positive impact on app launch. When fewer frameworks are loaded, it reduces the work dyld and the kernel need to do when launching your app, and it reduces memory usage, keeping your users happy. But we know separating code into libraries is vital for effective development and maintenance. With mergeable libraries, you can have both. Mergeable libraries make this possible with minimal code and configuration changes. And this scales nicely as you adopt newer frameworks. Let's revisit the earlier diagram about dynamic linking. All of these embedded frameworks can become mergeable since the linker can generate metadata for them. We can create a framework that merges the contents of the other libraries. So you end up with only one framework to embed. Dyld only needs to load that one library containing all segments across the embedded frameworks. Merging, in this way, can greatly simplify large dependency chains. That's what mergeable libraries can achieve. Let's talk about how to enable them. There's two ways to enable library merging in Xcode. I'll start with the simplest, automatic merging. Then, I'll get into manual merging for whenever you'd like to take control of what should be mergeable. I'll describe how mergeable libraries are able to provide optimal build times when in debug mode. And after, I'll share what to expect when you need to debug into and symbolicate your mergeable libraries. Automatic merging informs the build system to merge all direct dependencies that are embedded framework targets. It is especially useful on app targets. I'll show you. I'm using the Swift and C++ Forest Project as an example. There's an app target that links against four frameworks. There's SwiftUI, which comes from the Apple SDK. The other three, ForestBuilder, ForestUI, and Forest, are built in the project. When automatic merging is enabled, the three forest frameworks will become mergeable. SwiftUI is left as is since it's a system library. While linking the app, these frameworks will be merged directly into the app binary. That means these frameworks won't be needed at launch and can be removed from disk. Let's see how to turn this on in Xcode.
Inside the project, I've already clicked on the Swift and C++ app target and I'm inside the build settings tab. I need to update the MERGED_BINARY_TYPE build setting. I can use the filter text box to search for it.
"Create Merged Binary" is exactly what I want to update. It's the option that's mapped to the setting MERGED_BINARY_TYPE. I'll click on the setting and update its value to Automatic. And that's it! Mergeable library settings are under the General Linking options. They are conveniently displayed in their own section named "Linking - Mergeable Libraries." When an app is enabled for library merging, the segments of the libraries are linked directly into the app binary. This results in a similar performance to static libraries. But the exports of mergeable libraries are preserved in the app. It's often not applicable that apps export symbols and it negatively impacts the size and build time. To prevent this, use the linker option -no_exported_symbols. This can be applied in Xcode by updating Other Linker Flags with "-Wl, -no_exported_symbols." If your app needs entry points for app extensions, use an export list that lists those symbols to tightly control this. This can be set under the same General Linking options using Exported Symbols File. This allows the static linker to be the most effective for size optimizations like dead code stripping. That was automatic merging, but there can be times when only some of your frameworks should be merged together. Xcode supports this via Manual Merging. Manual merging is a fine-grained approach to specifying the libraries to merge. This is useful when some dependencies need to stay in the app bundle. I'll expand on this later when discussing considerations. It is enabled by setting MERGED_BINARY_TYPE = manual on the overarching target. The libraries that should make up the final merged product are recognized by setting MERGEABLE_LIBRARY to YES. And for libraries that should stay on disk, keep the default setting of MERGEABLE_LIBRARY to NO. Let's go back to the Swift and C++ Forest Project to demonstrate. We're starting fresh without any changes related to automatic merging. There's still the app target and the four frameworks it links against. But now, I'm also considering tests. There's an XCTest target and a support framework in the project. The tests also depend on the forest framework. Between the project's frameworks, the dependencies are all intertwined. In this example, we have an XCTest target, but in your project, you may have targets like app extensions that create a similar-looking dependency graph.
To leverage mergeable libraries, I'll isolate the app dependencies for the three forest frameworks.
I'll create a framework, ForestKit, that merges the libraries I need for the app but will also satisfy my test dependency.
ForestKit is considered a group library because it'll encapsulate the mergeable libraries both my app and tests depend on.
As I'm enabling manual mode, I'll explicitly set which frameworks to make mergeable. In this case, that's ForestBuilder, ForestUI, and Forest.
Those dependencies will merge into ForestKit. By reducing the libraries to load, my app has improved in launch time and bundle size. Let's turn this on in Xcode.
I've restarted the project and removed any settings used for automatic merging. I'll start by creating the framework target that will merge the other frameworks. This is my group library, ForestKit. I can do this by clicking at the bottom of the Targets section.
I'm in the macOS tab in the template pop-up, and I'll find the Framework template using the filter text box.
I'm going to set the product name to ForestKit and click Finish.
In this framework, I want to merge all libraries except for the Forest Test Support framework. But since my dependencies are intertwined, I'll link against all of them for the time being. To do this, I'll update the "Link Binary with Libraries" build phase to add the frameworks using the plus sign.
After the libraries pop-up has appeared, I'll click on the Forest framework and hold SHIFT and DOWN to highlight the other frameworks in the Xcode project.
Next, I need to enable manual merging on this target. I'll do this by going to the Build Settings tab and looking for "Create Merged Binary" again. I'll use the filter text box and type "MERGE." This time I'll select Manual as the value. That's everything I need to set on the group library target. I can select which libraries to merge by going to the build settings for each framework target. Navigating in the targets section, I'll start with the Forest framework. I'm in the Build Settings tab and can click Build Mergeable Library. This option is mapped to the build setting MERGEABLE_LIBRARY, and I'll update this value to Yes.
I need to do the same for ForestUI and Forest Builder.
I'm finished creating my merged ForestKit framework. But I need to update some dependencies. Because I've created a framework that encapsulates most of my dynamic libraries, I need to ensure my app and tests link against ForestKit and not the others. I'll fix up the app first by clicking the Swift and C++ App Target.
I'm going back to the Build Phases Tab and down to "Link Binary with Libraries." This is where I will remove the unnecessary frameworks. I select Forest and hold SHIFT and UP to collect ForestUI and ForestBuilder to delete them. The final step is the tests. I'll click on the XCTest target and go to the build phases tab under "Link Binary with Libraries." I'll remove the Forest framework by clicking the name in the table and deleting it.
Then I will add ForestKit using the plus sign.
Once the pop-up appears, I'll double-click ForestKit.
And this is how to configure manual merging. For the Swift and C++ Forest Project, I've been working in release mode. This is when libraries are merged then removed from disk. However, there is a build time overhead to merging that can get costly for development, similar to the build time behavior with static libraries.
To support iterative development in Xcode, the linker will not merge in debug mode. The build system tells the linker to reexport the libraries instead. Reexporting is a linker option that allows the implementation of code to live in one dynamic library but has it show up as if it's implemented in another. In other words, this means all of the libraries' APIs are reachable by just depending on the merged target, like your app extensions or tests. This results in a similar build time benefit as with dynamic libraries. At launch, dyld redirects any references to the reexported libraries instead of expecting them to come directly from the merged binary. That does mean in the debug case, the mergeable libraries stay on disk.
Speaking of debugging, let's look into a symbol that could be in a mergeable library. I have a function that takes in an integer and returns back its squared result. This is code that gets built. However, we know this is not the code a machine executes. Instead, this code goes through many transformations. This is all fine until we need to look into that code for bugs. This is why Xcode supports symbolication. Symbolication is the process of associating these machine instructions back to the original source code. This is useful to be able to understand crash logs or to profile and debug your code. How does this work with merged binaries? When you enable merging, source location information is still preserved from the original library. That means your debugging experience remains the same. But keep in mind, when library information is displayed, like for stack traces, it will show the path to the merged binary. This information is presented in crash logs, inside Instruments, and in the debugger. It's time to take into consideration how your own project could adopt mergeable libraries. In many cases, enabling them takes a few Xcode settings. But there are some factors worth noting. I'm going to cover five topics that are important to think about.
I'll begin with how you should handle any pre-existing dependencies you have on mergeable libraries. Then, I'll go into what autolinking is and how it works with mergeable libraries. And there are some restrictions I'll get into if you use runtime lookup APIs like dlopen or the bundle interface. Since merging is powered by the static linker in Xcode 15, I'll mention important differences between it and its predecessor. And the last consideration will be for folks interested in shipping their frameworks to other developers.
For library dependencies, let's go back to the diagram demonstrating dyld's work. If there are dependents of a mergeable library that are not merged-- for example, other executables-- they will need to update to depend on the merged framework, because mergeable ones are removed from disk. Another way this can come up is when an app relies on autolinking. Autolinking is a compiler option that's on by default. When the compiler finds module imports in source code, it detects framework dependencies to then pass to the linker. So if you're importing a module from a mergeable library, this could cause dynamic linking issues. You won't need to disable autolinking, though. The solution here is the same as before: link against the merged framework.
The most common way to do this is to add the merged framework in the "Link Binary with Libraries" Build Phase and remove the mergeable ones if it's there already. Otherwise, dyld won't be able to load the right frameworks for your app.
Most developers don't need to use dynamic linking APIs like dlopen. But if you do, those input paths will also need to point to the merged framework target.
Similarly, resource lookup could be impacted by library merging. This is because of what the runtime expects. In Swift, bundle is an API to have the runtime load a framework's bundle. The equivalent API in Objective-C is NSBundle's bundleForClass. These APIs are used to work with a framework's resources without having to consider the bundle's structure.
Up until iOS 12, the runtime needed the framework's binary to discover bundles, but mergeable frameworks won't have binaries in them by the time the process is running. Good news! In iOS 12, a hook was added to enable lookup for this scenario. That does mean if you rely on bundle lookup support, you should update your minimum deployment version to iOS 12 or later to use mergeable libraries. But if you don't rely on these APIs, you can disable this support with the new linker option -no_merged_libraries_hook. Then you won't need to update your app's deployment version. If you're merging frameworks that don't contain bundle resources, you also may not need the bundle hook. If this is the case, you should consider adding this option anyway to improve launch time performance. Throughout this talk, I've mentioned some new linker options. These options will only work with the newly implemented linker. But if you peek inside the toolchain, you'll notice there are two static linkers. The older linker is still supported for backwards compatibility. Most notably, that linker can still build for armv7k, but the new linker does not. The last platform to support the armv7k architecture was watchOS 8. If you don't need to deploy to watchOS 8 or earlier, upgrade the deployment version to watchOS 9 to use the new linker. I've described how to build and use mergeable libraries in your apps, but what if you want to ship a mergeable library for others to use? You can do this by creating an XCFramework in the Swift Package Manager or in Xcode. This allows you to build the framework including its metadata for distribution. When other developers use the framework, they can decide whether to enable merging. Mergeable metadata roughly doubles the size of the dylib. This doesn't impact the size of an app because metadata is discarded along with the mergeable library after building the app. Otherwise, that metadata does get stripped to prevent bloat when embedding them in apps. I've described some nuances to mergeable libraries. Now I'll share our recommendations. Setting dependencies on the merged binary is key to seamlessly adopting. This is necessary for any link dependencies. It is especially important if you feed libraries into tools that expect binaries during script phases. The static linker only merges direct dependencies. So, to include more mergeable libraries, you should set them as explicit link dependencies. The settings for merging instructs the Xcode build system to remove a framework's binary from disk. This will cause side effects if it's not intentional, so we recommend enabling them at the Xcode target level. And finally, to get the biggest benefit on productivity while still optimizing for performance, consider updating any static libraries that could be mergeable to dynamic. Mergeable libraries offer convenience and flexibility. Between automatic and manual workflows, you can restructure and add mergeable libraries at your leisure and leave the necessary ones on disk. This flexibility is helpful when gradually adopting or profiling. Mergeable libraries offer size, build, and runtime improvements when applied to framework and executable targets. You can have the build system merge all direct framework dependencies by using the automatic configuration. But when you need to pick and choose which dependencies to merge, this can be done with the manual mode. And lastly, when updating your project to use mergeable libraries, ensure all dependents of those libraries are relying on the merged binary instead of the libraries that get removed. For documentation about mergeable libraries, review "Configuring your project to use mergeable libraries." And to learn more about static and dynamic linking, check out the session "Link fast: Improve build and launch times." We are thrilled to learn how mergeable libraries will be used in your projects. Thank you for joining me.