Static vs Dynamic Framework Dispatching (Part 2 of 3): SPM vs Xcode frameworks
Observing What Actually Happens at Runtime
This is Article 2 of 3 in the series Static vs Dynamic Framework Dispatching.
In Article 1, we built a mental model of how static and dynamic frameworks are loaded and dispatched:
- Mach-O files
- dyld
- stubs, trampolines, and lazy symbol pointer tables
- mangled symbols
In this article, we stop theorizing and start measuring.
We will:
- Build the same SDK in static and dynamic forms
- Link them using SPM and Xcode Framework targets
- Inspect the produced binaries with
otool,nm,size, andlldb - Observe how dispatching changes:
- what is embedded
- what is loaded by
dyld - what is resolved lazily
This is not about “what Apple says”. This is about what your binary actually contains.
TL;DR
nmconfirms static framework symbols are inside the app binary (typeT) — dynamic framework symbols are not.otool -Lshows dynamic frameworks as separate loaded images — static ones don’t appear at all.otool -l | grep -c __got→1for dynamic,0for static — GOT entries provedyldpatching at launch.- Every dynamic call goes through a symbol stub in disassembly; static calls are direct
blinstructions. SPMandXcodeframework targets produce identical Mach-O dispatch patterns — the build tool doesn’t matter.lldb image listshows dynamic frameworks as standalonedyldimages; static framework code is invisible — merged into image[0].DEBUGbuilds inject a.debug.dylib— an Xcode overlay that can silently hide real linking errors.
Test Projects
As was mentioned in Article #1: all experiments in this article series use the following repository:
https://github.com/nikolay-dementiev/Library-linking_Static_Dynamic-Article
Let’s quick remind: it contains two setups:
1. Xcode Framework setup
- Client app:
Library-linking-Test-Xcode-Framework - Static framework:
XcodeFrameworkStatic - Dynamic framework:
XcodeFrameworkDynamic
2. Swift Package Manager setup
- Client app:
Library-linking-Test - Static package:
SDK-SPM-Static - Dynamic package:
SDK-SPM-Dynamic
Both SDKs expose the same API:
public struct MyMath {
public static func add(_ a: Int, _ b: Int) -> Int {
print("ADD from SDK")
return a + b
}
}The only difference is:
- how they are built
- how they are linked
Which makes them perfect for binary-level comparison.
What We Expect (Hypothesis)
If dispatching really differs:
| Variant | Expectation |
|---|---|
| Static | Symbols merged into app binary |
| Dynamic | Separate Mach-O loaded by dyld |
| Debug | Extra .debug.dylib wrapper |
| Release | Direct embedding or linking |
We will verify this with tools.
Setup our Test Project with SPM and Xcode Framework using Static / Dynamic Dispatching
SPM
With Swift Package Manager, we control how the library is linked by specifying the type of the product in Package.swift.
| Static | Dynamic |
|---|---|
![]() |
![]() |
That is essentially all we need to configure.
SPM will automatically link the package to the main app according to this setting.
So with SPM:
type: .static→ the library code is merged into the app binarytype: .dynamic→ the library is built as a separate.dyliband loaded at runtime bydyld
The rest of the linking process (build phases, search paths, embedding) is handled automatically by Xcode and SPM.
Xcode Framework
Compared to SPM, using an Xcode Framework target requires more manual configuration.
1. Choose how the framework is built (static vs dynamic)
We define this using Mach-O Type in the framework target’s Build Settings:
| Static | Dynamic |
|---|---|
![]() |
![]() |
This setting controls how the framework itself is produced:
- Static → the framework is archived and later merged into the app binary
- Dynamic → the framework is built as a separate Mach-O binary (
.framework/.dylib)
2. Choose how the framework is embedded into the app
Next, we configure how the main app target links and embeds this framework:
The important options here are:
-
Embed & Sign
This means:- the framework will be copied into the app bundle
- it will be code-signed
- it will be loaded at runtime by
dyld
This option is required for:
- dynamic frameworks
- any framework that must exist as a separate binary at runtime
-
Do Not Embed
This means:- the framework will NOT be copied into the app bundle
- its code is expected to already be part of the app binary
This option is correct for:
- static frameworks
- frameworks whose code is merged into the app at link time
we will play with those setting a bit in upcoming Article 3 to reveal some edge cases
So for Xcode Frameworks, we must configure two independent things:
- how the framework is built (
Mach-O Type) - how the app embeds or does not embed it (
Embed setting)
With SPM, both of these steps are abstracted away and handled automatically.
3. BTW: What Mach-O Type does the main app use?
The Mach-O Type for the main app target is always:
Executable
This means:
- the app itself is built as a runnable
Mach-Obinary - all static libraries and static frameworks are merged into this executable
- dynamic frameworks are loaded into this executable’s address space at runtime by
dyld
In other words:
the app is the final container where all linking strategies (
staticanddynamic) come together.
This is why all earlier choices (static vs dynamic framework, embed vs not embed) ultimately affect:
- what ends up inside the app executable
- and what must be loaded separately at runtime
Inspecting what type of Linked Images we use in our main app (using the otool -L command)
Let’s build the main app in DEBUG mode using a dynamic framework, then run:
otool -L Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-FrameworkExpected output pattern:
@rpath/Library-linking-Test-Xcode-Framework.debug.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)This shows that in
DEBUGmode the app is indirectly linked through the Dynamic Linker by using the{OUR-App-Name}.debug.dylibfile.
Now, let’s inspect the debug dylib itself:
otool -L Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework.debug.dylibWe should see something like (click to discover full response):
@rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic (compatibility version 1.0.0, current version 1.0.0)
..
/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 9126.2.4)
..
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
...
@rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic (compatibility version 1.0.0, current version 1.0.0)
..
/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 9126.2.4)
..
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
...@rpath/Library-linking-Test-Xcode-Framework.debug.dylib (compatibility version 0.0.0, current version 0.0.0)
@rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 4201.0.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)
/System/Library/Frameworks/DeveloperToolsSupport.framework/DeveloperToolsSupport (compatibility version 1.0.0, current version 23.0.4)
/System/Library/Frameworks/SwiftUI.framework/SwiftUI (compatibility version 1.0.0, current version 7.2.5)
/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 9126.2.4)
/usr/lib/swift/libswiftCore.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/swift/libswiftCoreFoundation.dylib (compatibility version 1.0.0, current version 120.100.0, weak)
/usr/lib/swift/libswiftCoreImage.dylib (compatibility version 1.0.0, current version 2.2.0, weak)
/usr/lib/swift/libswiftDarwin.dylib (compatibility version 1.0.0, current version 347.40.2, weak)
/usr/lib/swift/libswiftDispatch.dylib (compatibility version 1.0.0, current version 56.0.0, weak)
/usr/lib/swift/libswiftMetal.dylib (compatibility version 1.0.0, current version 370.64.2, weak)
/usr/lib/swift/libswiftOSLog.dylib (compatibility version 1.0.0, current version 10.0.0, weak)
/usr/lib/swift/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 951.1.0, weak)
/usr/lib/swift/libswiftQuartzCore.dylib (compatibility version 1.0.0, current version 5.0.0, weak)
/usr/lib/swift/libswiftSpatial.dylib (compatibility version 1.0.0, current version 1.0.0, weak)
/usr/lib/swift/libswiftUniformTypeIdentifiers.dylib (compatibility version 1.0.0, current version 879.2.4, weak)
/usr/lib/swift/libswiftXPC.dylib (compatibility version 1.0.0, current version 105.0.14, weak)
/usr/lib/swift/libswift_Concurrency.dylib (compatibility version 1.0.0, current version 0.0.0)
/usr/lib/swift/libswiftos.dylib (compatibility version 1.0.0, current version 1076.40.2, weak)
/usr/lib/swift/libswiftsimd.dylib (compatibility version 1.0.0, current version 23.0.0, weak)So, what does this mean?
- The app binary loads:
- a debug
dylib
- a debug
- The debug dylib loads:
- the dynamic framework
- The dynamic framework (our
.debug.dylib) remains its own separateMach-Oimage
And once again:
➡️ Dynamic framework = a separate image loaded by dyld
Xcode dynamic linking in DEBUG mode
DEBUG mode even for Static Frameworks?Let’s uncover an interesting Xcode behavior for the main app in a DEBUG build:
Remove the Dynamic Framework embedding.
Set:
Rebuild and run from Xcode.
Result:
✔️ The app works.
Stop debugging and run the freshly built app directly in the Simulator (not from Xcode), or copy the .app to another Simulator.
Result:
💥 The app crashes.
Console log:
...
Termination Reason: Namespace DYLD, Code 1, Library missing; Library not loaded: @rpath/XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic
...Why does this happen?
Because Xcode injects runtime paths and loader state when running in DEBUG.
We will analyze this behavior in detail in Article 3 (Xcode runtime tricks).
Static Framework Case
Now remove the dynamic framework and link only the static one:
Do not forget to:
comment out unused code (import) |
and | clear the Xcode cache |
|---|---|---|
![]() |
➕ | ![]() |
Then run:
otool -L Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework.debug.dylibExpected (click to discover whole response)
Library-linking-Test-Xcode-Framework.debug.dylib:
@rpath/Library-linking-Test-Xcode-Framework.debug.dylib (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 4201.0.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)
Library-linking-Test-Xcode-Framework.debug.dylib:
@rpath/Library-linking-Test-Xcode-Framework.debug.dylib (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 4201.0.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)/System/Library/Frameworks/DeveloperToolsSupport.framework/DeveloperToolsSupport (compatibility version 1.0.0, current version 23.0.4)
/System/Library/Frameworks/SwiftUI.framework/SwiftUI (compatibility version 1.0.0, current version 7.2.5)
/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 9126.2.4)
/usr/lib/swift/libswiftCore.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/swift/libswiftCoreFoundation.dylib (compatibility version 1.0.0, current version 120.100.0, weak)
/usr/lib/swift/libswiftCoreImage.dylib (compatibility version 1.0.0, current version 2.2.0, weak)
/usr/lib/swift/libswiftDarwin.dylib (compatibility version 1.0.0, current version 347.40.2, weak)
/usr/lib/swift/libswiftDispatch.dylib (compatibility version 1.0.0, current version 56.0.0, weak)
/usr/lib/swift/libswiftMetal.dylib (compatibility version 1.0.0, current version 370.64.2, weak)
/usr/lib/swift/libswiftOSLog.dylib (compatibility version 1.0.0, current version 10.0.0, weak)
/usr/lib/swift/libswiftObjectiveC.dylib (compatibility version 1.0.0, current version 951.1.0, weak)
/usr/lib/swift/libswiftQuartzCore.dylib (compatibility version 1.0.0, current version 5.0.0, weak)
/usr/lib/swift/libswiftSpatial.dylib (compatibility version 1.0.0, current version 1.0.0, weak)
/usr/lib/swift/libswiftUniformTypeIdentifiers.dylib (compatibility version 1.0.0, current version 879.2.4, weak)
/usr/lib/swift/libswiftXPC.dylib (compatibility version 1.0.0, current version 105.0.14, weak)
/usr/lib/swift/libswift_Concurrency.dylib (compatibility version 1.0.0, current version 0.0.0)
/usr/lib/swift/libswiftos.dylib (compatibility version 1.0.0, current version 1076.40.2, weak)
/usr/lib/swift/libswiftsimd.dylib (compatibility version 1.0.0, current version 23.0.0, weak)There is no reference to our Dynamic SDK at all.
Now try to search for its symbols:
nm Library-linking-Test-Xcode-Framework | grep MyMathExpected [pseudo code] (click to discover real command response)
_t_$s...MyMath...add
_t_$s...MyMath...add0000000000003fa8 T _$s20XcodeFrameworkStatic06MyMathC0V3addyS2i_SitFZ
00000000000040b0 T _$s20XcodeFrameworkStatic06MyMathC0VACycfC
0000000000005084 s _$s20XcodeFrameworkStatic06MyMathC0VMF
00000000000043d4 T _$s20XcodeFrameworkStatic06MyMathC0VMa
00000000000084c0 s _$s20XcodeFrameworkStatic06MyMathC0VMf
0000000000005048 S _$s20XcodeFrameworkStatic06MyMathC0VMn
00000000000084d0 S _$s20XcodeFrameworkStatic06MyMathC0VN
0000000000008468 s _$s20XcodeFrameworkStatic06MyMathC0VWV
00000000000040bc t _$s20XcodeFrameworkStatic06MyMathC0Vwet
000000000000420c t _$s20XcodeFrameworkStatic06MyMathC0Vwst
0000000000004d60 s _symbolic _____ 20XcodeFrameworkStatic06MyMathC0VThis means:
- ➡️ The static SDK’s code is physically merged into the app’s
Mach-O - No
dyldload - No runtime lookup
- Just direct jumps
Visual Model of Dispatching
Static Dispatch (Static framework / static SPM target)
Dynamic Dispatch (Dynamic framework / .dylib)
This difference explains:
| Framework | Static | Dynamic |
|---|---|---|
| Where is code? | merged into the main Mach-O |
extra Mach-O image + symbol resolution by dyld |
| Where is image? (approved with the otool) |
no separate image | visible as a separate image |
Swift Package Manager Comparison
Let’s build our main app with:
SDK-SPM-Static- and
SDK-SPM-Dynamic
So, our Test App folder structure looks like:
<Our App Folder>
├── ClientApp
│ └── Library-linking-Test
│ ├── Library-linking-Test
│ └── Library-linking-Test.xcodeproj
└── Framework
└── SDK-SPM
├── SDK-SPM-Dynamic
└── SDK-SPM-Staticand it’s XCode settings:
Lets inspect linking Static and Dynamic Frameworks with:
otool -L Library-linking-Test.debug.dylib | grep Dynamicand
otool -L Library-linking-Test.debug.dylib | grep StaticResult in accordingly:
Dynamic:
@rpath/SDK-SPM-Dynamic.framework/SDK-SPM-Dynamic (compatibility version 0.0.0, current version 0.0.0)Static:
No SDK reference.
Meaning:
➡️ SPM follows the same Mach-O rules
➡️ Dispatching is not an SPM feature
➡️ It is just a linker + loader behavior
CocoaPods (Conceptual Only)
It only controls how a dependency is packaged (static vs dynamic).
Actual dispatching still follows the same Mach-O + dyld rules as:
- Xcode frameworks
- Swift Package Manager products
Historically, CocoaPods used this switch:
use_frameworks!Which meant that Pods were built as dynamic frameworks
Without it Pods were built as static libraries
Modern CocoaPods behavior
Today, CocoaPods (no use_frameworks!) is static by default
If you add to your PODFile:
use_frameworks!Then the dependencies are built as dynamic frameworks
If you swap to:
use_frameworks! :linkage => :static-> Dependencies will be built as static frameworks again
So CocoaPods can produce static\dynamic frameworks depending on configuration.
Visual model: what CocoaPods really controls
Runtime model (after CocoaPods is done)
Important limitation
All of this applies only to pods built from source.
If a pod is distributed as a precompiled binary:
- CocoaPods cannot change its type
- it will remain whatever it was shipped as:
- static library, or
- static framework
Key takeaway for CocoaPods:
it does not:
- invent dispatching
- change symbol resolution
- change how
dyldworks
It only decides:
- how the dependency is built and linked
And once built, the runtime behavior is governed purely by:
Mach-Oformatlinkerdecisions- and
dyldloader logic
We do not deep-dive CocoaPods internals here to keep focus on dispatching itself.
Observing Lazy Binding
In Article 1 we built the mental model of lazy binding — the idea that a dynamic symbol is not resolved at launch, but on first use. The stub jumps to the GOT, dyld patches the real address, and every subsequent call goes directly.
Now let’s stop talking about it and actually see it inside our binaries.
What we’re looking for
Modern Apple binaries do not use __la_symbol_ptr (the classic lazy pointer table) anymore.
Instead, lazy binding lives in three places:
| Piece | Location | Role |
|---|---|---|
| Stub | __TEXT,__stubs |
tiny jump point called by your code |
| GOT entry | __DATA_CONST,__got |
pointer patched by dyld at runtime |
| dyld resolver | inside dyld itself |
finds the real address and patches the GOT |
So the question becomes:
“Can I actually find
__gotin my dynamic framework — and confirm it’s absent in the static one?”
Yes. Let’s do it!:
Inspect the GOT in a dynamic framework
Our app bundle looks like this:
Library-linking-Test-Xcode-Framework.app
├── Frameworks
│ └── XcodeFrameworkDynamic.framework
│ ...
│ └─── XcodeFrameworkDynamic ← dynamic Mach-O
...
├── Library-linking-Test-Xcode-Framework
└── Library-linking-Test-Xcode-Framework.debug.dylib ← thin launcher (Debug)
...Let’s look inside the dynamic framework:
otool -l XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic | grep -A4 __gotOutput:
sectname __got
segname __DATA_CONST
addr 0x0000000000004000
size 0x0000000000000038
offset 16384You’ll see a __DATA_CONST,__got section — so the GOT lives here.
Now dump its contents:
otool -v -s __DATA_CONST __got XcodeFrameworkDynamic.framework/XcodeFrameworkDynamicOutput:
XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic:
Contents of (__DATA_CONST,__got) section
Unknown section type (0x6)
0000000000004000 00000000 80100000 00000001 80100000
0000000000004010 00000002 80100000 00000003 80100000
0000000000004020 00000004 80100000 00000005 80100000
0000000000004030 00000006 80100000Those values are placeholder addresses — not real function pointers yet.
dyld will patch them with the real function addresses on the first call to each symbol.
After that — the stub reads the now-patched GOT entry and jumps directly to the function — no dyld involved anymore.
Find the stubs that use it
Now let’s look at the disassembly of our debug.dylib and grep for stubs:
otool -tvV Library-linking-Test-Xcode-Framework.debug.dylib | grep stubA fragment of the output:
...
00000000000012b8 bl 0x4b5c ; symbol stub for: _$s7SwiftUI12SceneBuilderV10buildBlockyxxAA0C0RzlFZ
00000000000015dc bl 0x4b2c ; symbol stub for: _$s21XcodeFrameworkDynamic06MyMathC0V3addyS2i_SitFZ
0000000000001604 bl 0x4c40 ; symbol stub for: _$ss26DefaultStringInterpolationV06appendC0yyxs06CustomB11ConvertibleRzlF
...Notice this line:
00000000000015dc bl 0x4b2c ; symbol stub for: _$s21XcodeFrameworkDynamic06MyMathC0V3addyS2i_SitFZThat mangled name — _$s21XcodeFrameworkDynamic06MyMathC0V3addyS2i_SitFZ — is our MyMath.add(_:_:).
The call goes through a stub at address 0x4b2c.
That stub loads a pointer from the GOT.
On first call — dyld resolves the symbol, patches the GOT entry with the real function address.
On every next call — the stub reads the already-patched GOT entry and jumps directly to the function.
This is lazy binding. Not a concept anymore — this is your binary.
Confirm the static framework has none of this
Now let’s check the static side. Run nm against the app binary linked with the static framework:
nm Library-linking-Test-Xcode-Framework.debug.dylib | grep MyMathYou'll see something like (click to expand):
0000000000004170 T _$s20XcodeFrameworkStatic06MyMathC0V3addyS2i_SitFZ
0000000000004170 T _$s20XcodeFrameworkStatic06MyMathC0V3addyS2i_SitFZ0000000000004278 T _$s20XcodeFrameworkStatic06MyMathC0VACycfC
0000000000005254 s _$s20XcodeFrameworkStatic06MyMathC0VMF
000000000000459c T _$s20XcodeFrameworkStatic06MyMathC0VMa
00000000000084c8 s _$s20XcodeFrameworkStatic06MyMathC0VMf
0000000000005218 S _$s20XcodeFrameworkStatic06MyMathC0VMn
00000000000084d8 S _$s20XcodeFrameworkStatic06MyMathC0VN
0000000000008470 s _$s20XcodeFrameworkStatic06MyMathC0VWV
0000000000004284 t _$s20XcodeFrameworkStatic06MyMathC0Vwet
00000000000043d4 t _$s20XcodeFrameworkStatic06MyMathC0Vwst
U _$s21XcodeFrameworkDynamic06MyMathC0V3addyS2i_SitFZ
0000000000004f34 s _symbolic _____ 20XcodeFrameworkStatic06MyMathC0VThe T means: defined in the text segment — the symbol lives directly inside the app binary.
No stub. No GOT entry. No dyld patching. Just a direct call at link time.
You can confirm this directly by counting __got occurrences in both binaries:
otool -l XcodeFrameworkStatic.framework/XcodeFrameworkStatic | grep -c __gotAND
otool -l XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic | grep -c __got| Binary | Output | Meaning |
|---|---|---|
XcodeFrameworkStatic |
0 |
no GOT — no dyld patching needed |
XcodeFrameworkDynamic |
1 |
__DATA_CONST,__got section present — dyld patches it at runtime |
What this looks like visually
Key takeaway
Lazy binding is not a
CocoaPodsfeature. Not anSPMfeature. Not anXcodefeature.
It is a Mach-O + linker + dyld mechanism — and now you’ve seen it with your own eyes.
| Dynamic framework | Static framework | |
|---|---|---|
GOT entry (__DATA_CONST,__got) |
✅ | ❌ |
Symbol stub (__TEXT,__stubs) |
✅ | ❌ |
dyld runtime patching |
✅ | ❌ |
| Direct call at link time | ❌ | ✅ |
Runtime confirmation: lldb image list
One more way to confirm: pause the app in lldb and run:
(lldb) image listEvery Mach-O image dyld has loaded into the process is listed. In this project — linking both XcodeFrameworkStatic and XcodeFrameworkDynamic — there are 734 entries total. The meaningful ones:
[ 0] C616C5D5-... 0x000000010206c000 .../Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework
...
[ 22] 14C85662-... 0x0000000102120000 .../Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework.debug.dylib
...
[ 87] 23F6AF64-... 0x0000000102110000 .../XcodeFrameworkDynamic.framework/XcodeFrameworkDynamic[0]— the app binary. All static framework code is inside it.[22]—*.debug.dylib— a DEBUG-only image Xcode injects for SwiftUI Previews / fast hot-reload. Not present in Release builds.[87]—XcodeFrameworkDynamic— our dynamic framework, mapped as its owndyldimage.
And XcodeFrameworkStatic? It does not appear in image list as a standalone image. You can confirm exactly where its code lives using image lookup:
(lldb) image lookup -rn XcodeFrameworkStaticDEBUG build — symbols resolved through the .debug.dylib overlay (image [22]):
4 matches found in .../Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework.debug.dylib:
Summary: Library-linking-Test-Xcode-Framework.debug.dylib`static XcodeFrameworkStatic.MyMathStatic.add(Swift.Int, Swift.Int) -> Swift.Int
Summary: Library-linking-Test-Xcode-Framework.debug.dylib`type metadata accessor for XcodeFrameworkStatic.MyMathStatic
...Release build — symbols resolved directly inside the main binary (image [0]):
4 matches found in .../Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-Framework:
Address: Library-linking-Test-Xcode-Framework[0x0000000100001e40] (__TEXT.__text + 3360)
Summary: Library-linking-Test-Xcode-Framework`static XcodeFrameworkStatic.MyMathStatic.add(Swift.Int, Swift.Int) -> Swift.Int
Address: Library-linking-Test-Xcode-Framework[0x0000000100001ef0] (__TEXT.__text + 3536)
Summary: Library-linking-Test-Xcode-Framework`getEnumTagSinglePayload value witness for XcodeFrameworkStatic.MyMathStatic
...In both cases the static framework’s code lives inside another image, never as a standalone dyld entry. In Release that image is [0] — the app binary itself.
Why This Matters (SwiftUI Context)
So far we’ve been looking at binary internals — GOT entries, stubs, mangled symbols.
But does any of this actually affect a real SwiftUI app?
Yes. And here’s where:
App startup time
Every dynamic framework your app links against must be loaded and resolved by dyld at launch — before your first SwiftUI view ever renders.
This is not free. On older devices or with many dynamic dependencies, this cost is measurable.
You can observe it yourself in Xcode’s launch time report in Instruments → App Launch.
Static frameworks skip this entirely — their code is already inside the app binary.
No dyld loading. No symbol resolution at launch. The first frame arrives faster.
This is why Apple’s own guidance recommends keeping the number of
dynamicframeworks low — especially for app targets where launch time matters.
Read more »
SwiftUI Previews and .debug.dylib
If you’ve ever wondered why SwiftUI Previews feel “magical” — here’s part of the answer.
Previews need to reload your code without restarting the simulator.
To do that, Xcode builds your app code into a .debug.dylib — a separate dynamic library that can be rebuilt and reloaded independently from the app binary.
This is why in DEBUG builds you see:
otool -L Library-linking-Test-Xcode-Framework.app/Library-linking-Test-Xcode-FrameworkOutput:
@rpath/Library-linking-Test-Xcode-Framework.debug.dylib (compatibility version 0.0.0, current version 0.0.0)
...Your app binary becomes a thin launcher.
The actual code — including your static frameworks — lives inside the .debug.dylib.
This means:
Xcodecan rebuild only the.debug.dylibon each change- Previews can reload the new
dylibwithout a full relaunch - Your
staticframeworks are temporarily treated asdynamicinDEBUG
We will go deep on
.debug.dylibin Article 3 — including why this can hide real linking errors that only surface inRELEASE.
Binary size
Static frameworks merge their code directly into the app binary.
Every symbol, every function — copied in at link time by the linker.
Dynamic frameworks keep their code in a separate Mach-O.
The app binary stays smaller — but the total .app bundle size includes the framework bundle.
The size command makes this structural difference visible:
size XcodeFrameworkStatic.framework/XcodeFrameworkStatic
size XcodeFrameworkDynamic.framework/XcodeFrameworkDynamicStatic — size lists each .o file separately, because a static framework is an archive of object files:
__TEXT __DATA __OBJC others dec hex
96 0 0 1503 1599 63f XcodeFrameworkStatic.framework/XcodeFrameworkStatic(XcodeFrameworkStatic_vers.o)
1396 176 0 5488 7060 1b94 XcodeFrameworkStatic.framework/XcodeFrameworkStatic(XcodeFrameworkStatic.o)Dynamic — a single linked Mach-O, one entry:
__TEXT __DATA __OBJC others dec hex
16384 0 0 49152 65536 10000 XcodeFrameworkDynamic.framework/XcodeFrameworkDynamicTwo things to notice:
- Static format: individual
.ofiles are visible — the framework is an archive, not a standalone binary. Total__TEXTis ~1.5 KB of actual code. - Dynamic
__TEXT = 16384: exactly one VM page (16 KB). Adylibmust be page-aligned, so even a minimal dynamic framework pays at least one page of overhead per segment, regardless of actual code size.
So the tradeoff is not “small vs large” — it’s where the code lives:
Static |
Dynamic |
|
|---|---|---|
| App binary size | larger | smaller |
Total .app bundle |
same code, different location | framework copied into Frameworks/ |
| Shared between processes | ❌ | ✅ (system frameworks only) |
Note: on iOS,
dynamicframeworks are not shared between apps — unlike macOS. Each app carries its own copy inside the.ipa. So the “shared memory” benefit applies only to Apple’s system frameworks (UIKit,SwiftUI,Foundationetc.).
Modular updates
As was briefly described in Article 1, on macOS (and server-side Swift), dynamic frameworks bring a real advantage:
a framework can be updated independently from the app binary.
This means:
- ship a bug fix in a framework — without recompiling the app
- multiple apps share the same
dylibin memory — loaded once bydyld
This is exactly how Apple’s own system frameworks work:
UIKit, SwiftUI, Foundation — one copy in memory, shared across all processes.
On iOS however, this advantage largely disappears:
App Storeapps are sandboxed- each app carries its own private copy of every
dynamicframework inside its.ipa - no sharing between apps
- updating a framework still requires resubmitting the app
So the “modular updates” argument is:
| Platform | Dynamic framework benefit |
|---|---|
| macOS / server | ✅ real — update framework without rebuilding app |
| iOS | ❌ limited — full app resubmission required anyway |
| System frameworks (Apple) | ✅ always — shared across all processes |
Key takeaway
Framework linking type affects your app in four concrete ways:
- Launch time —
dynamicframeworks adddyldresolution cost before first render - Previews —
Xcodewraps everything in a.debug.dylibto enable fast reload - Binary layout —
staticmerges into app binary,dynamiclives inFrameworks/ - Modular updates — real benefit on macOS, limited on iOS
These are not theoretical concerns.
You’ve already seen the stubs, the GOT entries, the .debug.dylib — with your own terminal commands.
What We Learned
We did not just read about static vs dynamic frameworks.
We opened real binaries, ran real commands, and saw real evidence.
Here is what the terminal told us:
| What we observed | What it means |
|---|---|
MyMath.add symbol inside app Mach-O (nm → T) |
static framework code merged at link time |
XcodeFrameworkDynamic appears in otool -L output |
dynamic framework loaded by dyld at runtime |
__DATA_CONST,__got section exists in dynamic framework |
GOT entries — patched by dyld on first call |
bl 0x... ; symbol stub for: _$s21XcodeFrameworkDynamic... |
every dynamic call goes through a stub |
static framework binary has no __got, no stubs (otool -l | grep -c __got → 0) |
direct calls only — no dyld involvement |
SPM follows the same Mach-O rules as Xcode frameworks |
dispatching is a linker + loader behavior, not a build tool feature |
CocoaPods use_frameworks! controls how a pod is packaged, not how dyld dispatches it |
CocoaPods is a builder — dyld rules are unchanged |
.debug.dylib appears in DEBUG build otool -L output |
Xcode wraps your app for fast reload and SwiftUI Previews |
The dispatch rules never change.
Only the containers do.
What Comes Next (Article 3)
In this article, we moved from theory to binary-level evidence.
We proved — with real terminal commands — that:
staticframeworks are merged into the appMach-Odynamicframeworks are loaded bydyldas separate images- lazy binding lives in
__DATA_CONST,__got— not in__la_symbol_ptranymore SPMandXcodefollow identicalMach-O+dyldrulesDEBUGbuilds wrap everything in a.debug.dylib
But one question remains:
“Why does the app still work in
DEBUGwhen I removeEmbed & Signfor adynamicframework —
but crashes when I run it outsideXcode?”
In Article 3, we will explain exactly that:
- what the
.debug.dylibreally is — and whyXcodecreates it - what
@rpathmeans and howXcodeinjects runtime loader paths inDEBUG - why
Embed & Signseems to be “ignored” inDEBUG— but is critical inRELEASE - why
DEBUGbuilds are not representative of real runtime behavior - and why this matters for
CI,TestFlight, andApp Storebuilds
Article 3 = the DEBUG vs RELEASE mystery. Solved.
Further Reading
-
Apple: Dynamic Libraries Overview
https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/OverviewOfDynamicLibraries.html -
Apple: Mach-O Programming Topics
https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachOTopics -
Apple: Bundle Types
https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html -
Static vs Dynamic Libraries (Poplauschi)
https://bpoplauschi.github.io/2021/10/25/Advanced-static-vs-dynamic-libraries-and-frameworks.html
Next: Article 3 — Why Debug Lies to You: dyld, rpaths, and the Debug Dylib







