SwiftQARandom

What's the difference between static and dynamic linking on iOS?

Static linking copies library code into your executable at build time. Dynamic linking leaves the code in a separate binary that’s loaded at launch (or on demand) by dyld.

Static (.a archives, static frameworks, SPM static products)

  • The linker resolves symbols at build time and bakes the code into the app binary.
  • No load-time cost for resolving that library, so launch is faster.
  • Dead-code stripping can remove unused code, which helps binary size.
  • Each target that statically links the same library gets its own copy. If both the app and an extension link it, you get duplicate code. If two frameworks each statically link the same library, you can get duplicate symbols or duplicate Objective-C class warnings, plus subtle bugs around singletons and global state.

Dynamic (.dylib, dynamic frameworks, XCFrameworks marked “Embed & Sign”)

  • Code lives in a separate Mach-O image inside the app bundle, or in the OS (UIKit, Foundation, etc.).
  • dyld finds, maps, and binds it at launch. Each dynamic framework adds to pre-main launch time, and Apple’s guidance is to keep the count low — roughly a handful.
  • It can be shared between the app and its extensions, which avoids duplication and gives a single copy of global state.
  • On iOS, third-party dynamic frameworks must be embedded in the app bundle, since apps can’t share them across apps the way system frameworks are shared. The “update the library without rebuilding” benefit only applies to Apple’s own system libraries, which is why apps always link dynamically against UIKit.

Why it matters in practice

  • Launch time. Merge or statically link most internal modules. Keep dynamic only where there’s a reason, such as sharing code with extensions or a very large binary.
  • Modularization. With Swift Package Manager, library products default to automatic, meaning Xcode decides. Being explicit avoids surprises. Mergeable libraries (Xcode 15+) let you build modules dynamically in debug for fast incremental builds, then merge them into the main binary in release for static-like launch performance.
  • Duplicate symbols. Check for the same static dependency pulled into multiple frameworks. The fix is to make the shared dependency dynamic, or restructure so it’s linked once.
  • Licensing. The LGPL effectively requires dynamic linking, so this can be a real constraint for some third-party code.
  • Binary size and stripping. Static gets better dead-stripping. Dynamic frameworks ship whole.

What dyld does at launch

For every dynamic image, dyld loads the Mach-O, maps dependent dylibs, rebases and binds pointers, and runs initializers — the mechanics behind why dynamic linking has a real, measurable launch-time cost. It’s also possible to load code dynamically at runtime via dlopen, but not for downloaded code on iOS, since App Store rules prohibit it.

One-liner if pressed: “Static trades binary duplication risk for faster launch. Dynamic trades launch time for sharing and isolation. The default is static for app-internal modules, dynamic when code must be shared across targets, and mergeable libraries to get both in debug and release.”