静态库 & 动态库

image.png

  1. 静态库(Static Library):通常能减少 dyld 需要单独处理的自定义 image 数量,但不能据此断言它一定让冷启动更快。

  2. 动态库(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,不是静态库必快或动态库必慢的标签。”

这样回答,既解释了基础,又反驳了误区,还把话题引回了你擅长的二进制重排,体现了技术深度。