静态库 & 动态库

-
静态库(Static Library):通常能减少 dyld 需要单独处理的自定义 image 数量,但不能据此断言它一定让冷启动更快。
-
动态库(Dynamic Library):自定义 Embedded Framework 越多,dyld 的映射、依赖解析、ObjC/Swift 元数据初始化等工作通常越多。历史 WWDC 曾给出过减少动态库数量的经验建议,但“6 个”不是适用于所有系统版本和项目的硬性上限,最终要以启动测量结果为准。
1. 静态库 vs 动态库:核心区别
可以用 “盖房子” 来比喻:
1.1 静态库 (.a, .framework 的 Mach-O Type 为 Static)
-
链接方式(Copy):
-
在编译链接阶段,链接器(Linker)会将静态库中的代码(指令)完整拷贝到你的 App 可执行文件(Mach-O)中。
-
比喻:像砌墙。你买了现成的砖块(静态库),直接砌进你的墙体里。房子盖好后(编译完),砖块就成了墙的一部分,以后不需要再单独搬运砖块了。
-
-
内存表现:
- 每个 App 都有自己独立的一份代码副本,不共享。
1.2 动态库 (.dylib, .framework 的 Mach-O Type 为 Dynamic)
-
链接方式(Reference):
-
链接器不拷贝代码,只在主程序里留一个引用(指针/签名)。等到 App 运行时(Runtime),系统(dyld)才去加载这个库。
-
比喻:像借工具。你盖房子时只写了个条子“我要用电钻”(引用)。等房子真正要装修(启动运行)时,你才去工具房把电钻拿过来用。
-
-
内存表现:
-
系统库(UIKit, Foundation):系统共享一份,所有 App 共用(通过 Shared Cache 技术),速度极快。
-
自定义库(Embedded Framework):虽然叫动态库,但在 iOS 沙盒机制下,实际上每个 App 也是各自拷贝一份在自己的 Bundle 里,并不能跨 App 共享内存(除非是系统级的)。
-
| 特性 | 静态库 (Static) | 动态库 (Dynamic) |
| 文件格式 | .a, .framework | .dylib, .framework |
| 链接行为 | 编译时拷贝代码 | 运行时加载引用 |
| App 包体积 | 较大(代码在主程中) | 较小(主程小,但加上库文件总体积差不多) |
| 加载时机 | 随主程序启动加载 | dyld 单独加载、链接 |
2. 为什么静态库不等于“零启动成本”?
你担心的点可能是:“静态库合并进去了,主 Mach-O 变大了,读取不需要时间吗?”
2.1 虚拟内存的“按需加载”机制
还记得我们上一篇讲的 缺页中断(Page Fault) 吗?
-
操作系统不会在启动时把整个 100MB 的 Mach-O 文件一次性读入 RAM。
-
它只会读取启动那一刻需要执行的代码。
-
更准确的结论:未触达的代码页通常不会全部读入物理内存,但更大的主二进制仍可能增加代码签名校验、Mach-O 元数据处理、映射范围、启动闭包处理、页错误和缓存局部性成本。影响大小取决于真正被链接进来的内容和启动路径,不能写成“完全不影响”。
静态库也不是把整个 .a 无条件复制进主程序。链接器通常以 object file 为粒度抽取满足符号引用的成员;-all_load、-force_load、-ObjC、dead code stripping 和链接时优化都会改变最终进入 Mach-O 的内容。
2.2 减少了 dyld 的工作量(关键优势)
冷启动的 Pre-main 阶段,dyld(动态链接器)非常忙。如果我们使用 10 个自定义动态库:
-
Load dylibs:dyld 要查找、验证签名、映射 10 个文件到内存。
-
Rebase/Bind:每个动态库都有自己的符号表,dyld 需要修正 10 个库里的指针偏移(Rebase)和外部符号绑定(Bind)。这部分是 O(n) 复杂度,消耗大量 CPU。
-
ObjC Setup:需要注册 10 个库里的类、Category。
如果换成静态库:
这 10 个库中最终被链接的代码合并进主程序。对 dyld 来说,它们不再是 10 个需要独立装载的自定义 image,但主程序自身仍然要被映射和修正,系统依赖也依旧存在。
-
减少对多个独立 Embedded Framework 的映射和元数据处理,并不代表完全没有代码签名或页校验成本。
-
跨 image 的绑定工作通常减少,部分内部引用可在链接期确定;主二进制和它依赖的动态库仍可能包含 fixups。
-
减少文件映射与随机 I/O 的机会。这里的主要矛盾不是普通意义上的“上下文切换”,而是 image 数量、fixups、元数据注册和启动阶段实际触达的页面。
3. 静态库常见的成本与优化方向
静态链接常常有利于减少 image 数量,但它的成本不只有代码布局分散:
-
链接参数不当可能把大量无用 object file 带进包内。
-
重复静态链接到多个动态 framework,可能产生代码重复。
-
主二进制变大可能影响下载体积、安装、代码签名页和启动阶段的局部性。
-
+load、C/C++ constructor、Swift/ObjC 元数据等初始化工作不会因为“来自静态库”就自动消失。 -
问题:
静态库的代码被简单追加在主程序末尾。如果启动时你需要频繁调用静态库里的 funcA,而 funcA 被链接到了文件的第 1000 页。-
CPU 就要跳到第 1000 页去读(触发 Page Fault)。
-
如果静态库很大,且启动代码分散在各个角落,就会导致大量的 Page Fault。
-
-
解法(完美闭环):
这正是 二进制重排(Binary Reordering) 发挥作用的地方!-
通过二进制重排,我们可以把静态库里那些“启动时真正用到的函数”,抠出来放到 Mach-O 的最前面(第 1 页)。
-
组合思路:减少不必要的动态 image,再根据真实启动 trace 做代码布局优化。二进制重排只能改善特定页面访问局部性,无法替代删除
+load、延迟初始化和缩短首屏业务链路。
-
4. 面试满分总结话术
如果面试官问:“静态库和动态库有什么区别?静态库包体积大,会不会拖慢启动?”
建议回答:
-
区别定义:
-
静态库在编译链接期将代码完整拷贝到可执行文件中;
-
动态库在编译期只保留引用,运行时由 dyld 加载。
-
在 iOS 中,除了系统库外,自定义动态库无法在 App 间共享,因此相比静态库优势不明显。
-
-
启动性能分析(破除误区):
-
静态链接通常能减少自定义动态 image 的装载成本,但“文件按需分页”不等于二进制大小对启动完全无影响。
-
使用静态库可能减少 dyld 需要单独处理的 image、fixups 和元数据,但具体收益随系统的 dyld 版本、启动闭包和项目依赖关系变化。不要把历史经验值当成固定上限。
-
-
进阶补充:
- “我会先用 Instruments、MetricKit 或启动埋点确认耗时是在 image 装载、静态初始化、页错误还是首屏业务。动态库过多就治理依赖和 image 数量;启动代码页分散再评估二进制重排。优化依据是 trace,不是静态库必快或动态库必慢的标签。”
这样回答,既解释了基础,又反驳了误区,还把话题引回了你擅长的二进制重排,体现了技术深度。