iOS 启动性能优化:深入解析 dyld 加载机制与演进
作为 iOS 开发者,我们每天点击 Xcode 的 Run 按钮,App 便迅速启动。这背后,苹果的动态链接器 dyld 默默完成了一系列复杂的加载与链接工作。理解 dyld 的加载流程及其演进历史,是进行 性能调优 和 系统优化 的关键。本文将从编译过程出发,深入剖析 dyld 的工作机制与版本迭代。
从源码到可执行文件:编译四部曲
在 Xcode 中点击 Build,编译器会依次执行四个步骤:
- 预处理:处理
#开头的指令,如展开头文件、删除注释,生成.i文件。 - 编译:对
.i文件进行词法、语法和语义分析,执行 代码优化,生成汇编代码.s文件。
词法分析:将源代码的字符序列分割成一个个的token(关键字、标识符、字面量、特殊符号),例如将标识符放到符号表中
语法分析:生成抽象语法树AST,此时运算符号的优先级确定,一些符号的多重语义也确定了。如果出现语法错误就会报错
静态分析:分析类型声明和匹配问题。
中间语法生成:LLVM编译器先生成一种中间语言LLVM IR,可以在编译期就做一些表达式的优化
目标代码生成与优化:将IR语法转换为机器依赖的汇编语言,并优化,如果这个过程中有变量切定义在同一个编译单元中就会给这个变量分配空间,确定变量地址。如果不在就等链接时确定。
- 汇编:将
.s文件翻译为机器码,输出目标文件.o。 - 链接:将多个
.o文件与系统库、框架合并,生成可执行文件。

链接与装载是 dyld 的核心舞台,下面我们重点展开。
静态库与动态库:选择与权衡
库文件分为静态库和动态库,它们在链接阶段的行为截然不同,直接影响 App 的包体积和启动性能。
链接的主要内容就是将各个模块之间的相互引用部分处理好,使得各个模块之间能够正确衔接。链接过程包括了地址和空间的分配、符号决议和重定位,链接器将经过汇编器编译成的所有目标文件和库进行链接成最终的可执行问津啊,而最常见的就是运行时库。所以库说白了就是一组常用代码编译成目标文件再经过链接后的存放
可执行文件是一个静态的概念,运行之前只是硬盘上的一个文件,而进程是一个运行时的一个过程,每个程序运行起来后,都会拥有自己的独立的虚拟地址空间,这个地址空间的大小是由计算机的硬件决定的。但是程序运行在系统上是不可能任意使用全部的虚拟空间的,操作系统为了实现监控程序运行等目的,进程的虚拟空间都会被操作系统控制。并且进程的虚拟地址空间是彼此隔离的,如果出现越界访问,会强制结束进程。将硬盘上的可执行文件映射到虚拟内存的过程就是重载。由于内存的稀有,研究发现程序运行时是有局部性原理的,可以只将常用的部分驻留在内存中,不太常用的数据放在磁盘里,即动态装载的基本原理,覆盖载入和页映射就是两种经典的动态装载方法。
这个过程中,操作系统主要做三件事:
- 创建一个独立的虚拟地址空间
- 读取可执行文件头,并建立虚拟空间与可执行文件的映射关系
- 将CPU的指令寄存器设置为可执行文件的入口地址,并启动执行
静态库(.a / .framework)
静态库在链接时,链接器会将实际用到的目标代码拷贝到可执行文件中。链接完成后,运行时不再依赖库文件本身。
- 优点:编译后无外部依赖,可独立运行。
- 缺点:多份拷贝导致 包体积增大,对内存和性能消耗较大。
静态库本质上不是运行时加载的独立模块,而更像是一个目标文件集合包。静态库不是运行时加载一个库文件,而是在构建最终程序时,将需要的代码拷贝进去。
静态库中的可执行代码会被链接到可执行文件的代码段中,与主程序的代码一起存储在这个段中。
静态库中的初始化数据会被链接到可执行文件的数据段中。
静态库中的未初始化数据会被链接到 BSS 段中。
动态库(.dylib / .framework / .tbd)
动态库在编译时只存储引用,运行时才被载入。
- 优点:减小包体积,共享内存,节约资源;可通过更新动态库实现程序更新。
- 缺点:动态载入带来性能损失,且依赖外部环境,版本不匹配会导致程序无法运行。
需要注意的是,.framework 格式既可以是静态库也可以是动态库,区别在于是否包含 Mach-O 文件。使用 file 命令可以查看其类型。

体积小于最小单位16块的静态库编译踹出来的动态库统计等于16块
虽然动态库会导致一些速度降低,但是会通过延迟绑定(lazy binding)技术优化

dyld 加载流程:自举、装载与链接
dyld(the dynamic link editor)是苹果的动态链接器,负责加载和链接 Mach-O 可执行文件。其加载流程可分为几个关键阶段:
- 重定位:将代码和数据中的地址修改为当前进程可用的实际地址。
- 自举:动态链接器启动时,通过一段特殊代码完成自身的重定位,不依赖全局变量或函数调用。
- 符号表合并:将可执行文件和自身符号表合并为全局符号表。
- 依赖装载:使用广度优先算法递归装载所有依赖的动态库。
- 重定位与初始化:修正所有符号地址,运行初始化器,最终将控制权交给程序入口。

dyld::_main 源码深度分析
在 dyld 源码中查找 __dyld_start,可以发现它调用了一个 start 方法。

该方法的核心步骤包括:
- dyld 自举,修正内部定位。
- 初始化栈保护值和随机信息。
- 运行 dyld 自己的 C++ 初始化器。
- 获取主程序的 slide,为后续处理做准备。

rebaseDyld(dyldsMachHeader);
进入 _main 函数后,依次执行:
- 环境变量配置:根据环境变量设置相关值,获取当前运行架构。
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)mainExecutableMH, 0, 0);
}
//Check and see if there are any kernel flags
dyld3::BootArgs::setFlags(hexToUInt64(_simple_getenv(apple, "dyld_flags"), nullptr));
// Grab the cdHash of the main executable from the environment
//配置相关环境操作
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
//通过_simple_getenv(apple, "...")从apple[]中按key取值:读取dyld启动标志、读取主程序的cdHash、读取dyld和主程序对应的文件路径,用户通知内核/追踪
mainExecutableCDHash = mainExecutableCDHashBuffer;
#if !TARGET_OS_SIMULATOR
// Trace dyld's load
notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
// Trace the main executable's load
notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif
uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;
sMainExecutableSlide = mainExecutableSlide;- 共享缓存检查:检查是否开启共享缓存(如 UIKit、CoreFoundation 等),并映射到共享区域。
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache();
#else
mapSharedCache();
#endif
}//libSystem、Foundation、UIKit/AppKit、libobjc、其他系统framework。操作系统在访问这些系统库数据时,不会让每个进程单独去磁盘上执行查找解析等操作,而是提前将这些常用系统库整理成一个大的共享文件。- 主程序初始化:调用
instantiateFromLoadedImage实例化ImageLoader对象。
//主程序的Mach-O已经被内核提前映射到内存,dyld现在创建一个ImageLoader对象管理它(将裸的Mach-O映像包装成dyld内部可管理的对象)
// The kernel maps in main executable before dyld gets control. We need to
// make an ImageLoader* for the already mapped in main executable.
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
}
throw "main executable not a known format";
}- 插入动态库:遍历
DYLD_INSERT_LIBRARIES环境变量,调用loadInserted加载。
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}//修改这个配置可以让自己写的库被加载- 重定位与初始化:完成所有需要重定位的库,然后初始化主程序。
// let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// initialize this image
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);最后,notifySingle 函数触发 load_images 回调,执行 call_load_methods 循环调用各类的 load 方法。随后 doModInitFunctions 调用全局 C++ 对象构造函数,最终返回主程序入口函数。
[AFFILIATE_SLOT_1]
dyld 演进史:从 1.0 到 4.0 的性能进化
dyld 的发展是一部典型的性能进化史,核心目标是:减少启动时的重复计算,提升加载速度。
dyld 1.0:预绑定时代
早期版本使用 预绑定(Prebinding) 技术,为所有动态库分配固定内存地址。若地址未变,可直接加载,避免重新计算偏移。
⚠️ 缺陷:安全性差,维护困难,系统库更新后需重新预绑定。
dyld 2.0:共享缓存与 ASLR
dyld 2 是应用最久的版本(至 iOS 12),引入两大核心改进:
- 共享缓存(Dyld Shared Cache):将系统库合并为一个大文件,App 启动时直接映射,节省 500MB-1GB 内存,显著加快加载速度。
- ASLR 与代码签名:地址随机化防止攻击,代码签名确保库未被篡改。
工作流程(进程内同步执行):
1. 内核启动进程,把控制权交给 dyld
2. dyld 自身初始化,解析主程序 Mach-O 头和运行环境
3. 为主可执行文件建立 image loader
4. 检查 dyld shared cache 和 dyld 环境配置(如 DYLD_INSERT_LIBRARIES)
5. 递归加载主程序依赖的所有 dylib
6. 对所有 image 做 rebase / bind / 其他 fixups
7. ObjC runtime 根据 dyld 回调完成类、分类、协议、selector 注册
8. 按“先依赖后自身”执行各 image 的初始化逻辑(constructor / +load 相关流程)
9. 读取主程序的 LC_MAIN load command,取得入口地址
10. 跳转到入口,最终进入 main缺点:所有操作在主线程串行执行,库多时明显阻塞启动。
dyld 3.0:启动闭包与进程外解析
WWDC 2017 推出,iOS 13 起强制用于第三方 App。核心思想:将耗时操作移出启动过程。

- 启动闭包(Launch Closure):App 安装或更新时,后台守护进程预先解析 Mach-O 文件,生成二进制闭包,包含依赖图、符号地址等所有静态信息。
- 进程内引擎:启动时直接映射闭包,无需解析 Mach-O 头或查找符号。
✅ 性能提升:冷启动速度提升约 30%+。
⚠️ 局限性:若 App 或依赖库被修改(如热修复),闭包失效,需回退到慢速模式。
dyld 4.0:双模式加载
iOS 16 / macOS 13 引入,结合 dyld 2 的灵活性与 dyld 3 的高性能。
- PrebuiltLoader:轻量级预构建加载器,从缓存中加载。
- JustInTimeLoader:实时解析模式,当预构建失效时无缝切换。
✅ 优势:兼顾速度与灵活性,完美适配热修复与频繁更新场景,内存占用更低。
[AFFILIATE_SLOT_2]
总结
dyld 从 1.0 到 4.0 的演进,展现了苹果在 启动性能优化 上的持续投入:从预绑定到共享缓存,再到启动闭包与双模式加载,每一次迭代都旨在减少重复计算、提升缓存命中率。理解这些原理,有助于我们在开发中进行更精准的 查询优化 与 系统优化,打造启动更快、体验更佳的 App。
浙公网安备 33010602011771号