动态库加载是Pre-main阶段的重要组成部分,每个动态库都需要加载、验证签名、进行符号绑定,这些操作会显著影响启动时间。


问题分析

当App启动时,dyld需要:

  1. 分析Mach-O文件的Load Commands,找出依赖的动态库
  2. 递归加载所有依赖的动态库
  3. 对每个动态库进行签名验证
  4. 执行Rebase和Bind操作

动态库数量越多,这些操作的耗时就越长。Apple建议将自定义动态库数量控制在6个以内。关于动态库加载的详细流程,可以参考Mach-O的链接、装载与库

关于缓存机制

  • 系统动态库:已被放入 dyld shared cache 中,其加载和符号绑定操作已预先完成,不会影响 App 启动时间
  • App 动态库:dyld 3 引入的 Launch Closure 机制会缓存依赖分析、Rebase/Bind 信息等元数据。首次启动(或 App 更新后)会生成缓存,后续启动直接使用

但即使有 Launch Closure 缓存,Rebase/Bind 操作本身仍需执行(因为 ASLR slide 每次启动都不同),动态库数量越多,这些操作的耗时仍然越长。

App可执行文件
    ├── UIKit.framework
    │     ├── Foundation.framework
    │     │     └── CoreFoundation.framework
    │     └── CoreGraphics.framework
    ├── 自定义Framework A
    │     └── 依赖库...
    └── 自定义Framework B
          └── 依赖库...

优化方案

1. 合并动态库

将功能相近的动态库合并为一个:

优化前:
App
├── LibA.framework
├── LibB.framework
├── LibC.framework
├── LibD.framework
└── LibE.framework
(5个动态库,加载耗时约 250ms)

优化后:
App
├── CoreLib.framework (合并 A, B, C)
└── UtilLib.framework (合并 D, E)
(2个动态库,加载耗时约 100ms)

2. 动态库改为静态库

将非必要的动态库改为静态库,静态库在编译时就会链接到可执行文件中,不需要运行时加载:

库类型编译时运行时对启动的影响
动态库记录依赖加载、链接增加启动时间
静态库合并到可执行文件无额外操作无影响

在CocoaPods中配置静态库:

# Podfile
use_frameworks! :linkage => :static

# 或者针对特定Pod
pod 'SomePod', :linkage => :static

3. 使用Swift Package Manager静态链接

SPM默认使用静态链接,这对启动优化是有利的:

// Package.swift
let package = Package(
    name: "MyPackage",
    products: [
        .library(
            name: "MyLibrary",
            type: .static,  // 明确指定静态库
            targets: ["MyLibrary"]),
    ],
    // ...
)

4. 移除不再使用的动态库

定期审查项目依赖,移除不再使用的库:

检查步骤:
1. 在Build Phases → Link Binary With Libraries中查看所有链接的库
2. 使用otool -L查看实际链接的动态库
3. 移除未使用的库引用

使用命令行检查:

# 查看App链接的所有动态库
otool -L /path/to/YourApp.app/YourApp