启动优化-减少动态库

动态库加载是Pre-main阶段的重要组成部分,每个动态库都需要加载、验证签名、进行符号绑定,这些操作会显著影响启动时间。 问题分析 当App启动时,dyld需要: 分析Mach-O文件的Load Commands,找出依赖的动态库 递归加载所有依赖的动态库 对每个动态库进行签名验证 执行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. 合并动态库 将功能相近的动态库合并为一个: ...

May 2, 2026

启动优化-load方法

+load方法是ObjC运行时在类被加载时自动调用的方法,所有+load方法在main函数之前同步执行,会阻塞启动。 问题分析 +load方法的特点: 特性 说明 调用时机 main函数之前,ObjC Runtime初始化时 调用顺序 父类 → 子类 → Category 线程 主线程,同步执行 影响 直接阻塞启动 // 问题代码:+load中执行耗时操作 @implementation HeavyModule + (void)load { // 耗时操作会阻塞启动 [self setupDatabase]; [self preloadResources]; [self registerServices]; } @end +load与+initialize的区别 特性 +load +initialize 调用时机 main之前 类首次使用时 调用次数 只调用一次 可能多次(子类触发) 是否阻塞启动 是 否 线程安全 是 是 调用顺序 父类→子类→Category 父类→子类 更详细的对比请参考:+load与+initialize的区别 优化方案 方案1:使用+initialize替代 将+load中的逻辑迁移到+initialize,延迟到类首次使用时执行: @implementation HeavyModule // 优化前 + (void)load { [self setupDatabase]; } // 优化后 + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 延迟到类首次使用时执行 [self setupDatabase]; }); } @end 注意:+initialize可能被子类触发多次调用,需要使用dispatch_once保证只执行一次。 ...

May 2, 2026

启动优化-didFinishLaunching

didFinishLaunchingWithOptions是main阶段的核心入口,在这里初始化大量SDK和服务会阻塞启动。本文介绍如何优化这个阶段的耗时。 问题分析 典型的问题代码: func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // 所有初始化都在主线程同步执行 CrashReporter.setup() Analytics.setup() PushNotification.setup() NetworkManager.setup() DatabaseManager.setup() ImageCache.setup() AdSDK.setup() SocialSDK.setup() // ... 更多SDK return true } 这种写法的问题: 所有初始化都在主线程同步执行 无论是否需要,所有SDK都在启动时初始化 阻塞首帧渲染 优化方案 方案1:分级启动任务管理 将启动任务按优先级分类,只在启动时执行必要的任务: // 启动任务优先级 enum LaunchTaskPriority { case required // 必须在首帧前完成 case high // 首帧后立即执行 case normal // 首屏稳定后执行 case low // 空闲时执行 } // 启动任务管理器 class LaunchTaskManager { static let shared = LaunchTaskManager() private var tasks: [LaunchTaskPriority: [() -> Void]] = [:] func register(priority: LaunchTaskPriority, task: @escaping () -> Void) { if tasks[priority] == nil { tasks[priority] = [] } tasks[priority]?.append(task) } func executeRequiredTasks() { tasks[.required]?.forEach { $0() } } func executeHighPriorityTasks() { DispatchQueue.main.async { self.tasks[.high]?.forEach { $0() } } } func executeNormalTasks() { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.tasks[.normal]?.forEach { $0() } } } func executeLowPriorityTasks() { // 监听RunLoop空闲时执行 CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode) { self.tasks[.low]?.forEach { $0() } } } } 使用示例 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let manager = LaunchTaskManager.shared // 必须在首帧前完成 manager.register(priority: .required) { CrashReporter.setup() // 崩溃收集必须最先初始化 } // 首帧后立即执行 manager.register(priority: .high) { Analytics.setup() PushNotification.setup() } // 首屏稳定后执行 manager.register(priority: .normal) { AdSDK.setup() SocialSDK.setup() } // 空闲时执行 manager.register(priority: .low) { PreloadManager.preloadResources() } // 只执行必须的任务 manager.executeRequiredTasks() return true } 任务分级建议 优先级 适用场景 示例 required 崩溃收集、核心功能依赖 Crash SDK、必要的配置加载 high 用户可能很快用到 推送、统计、网络配置 normal 非首屏功能 广告SDK、社交分享 low 预加载、缓存 图片预加载、数据预热 方案2:并行初始化 将无依赖关系的初始化任务并行执行: ...

May 2, 2026

启动优化-Rebase与Bind

Rebase和Bind是Pre-main阶段的重要步骤,用于修正程序中的指针地址。ObjC类、方法、协议等元数据越多,需要修复的指针越多,启动时间越长。 基本概念 由于ASLR(Address Space Layout Randomization)技术,App每次启动时加载到内存的地址都是随机的,因此需要进行地址修正。关于Rebase和Bind的底层原理,可以参考Mach-O的链接、装载与库。 Rebase(重定位) 修正指向Mach-O内部的指针,将编译时的地址加上ASLR偏移量。 Bind(绑定) 修正指向Mach-O外部的指针,查找符号表,绑定到正确的外部符号地址。 flowchart LR subgraph compile["编译时 (Mach-O文件)"] A1["内部指针: 0x1000(指向内部数据)"] A2["外部符号: _objc_msgSend(待绑定)"] end subgraph runtime["运行时 (内存中)"] B1["内部指针: 0x1000 + slide(Rebase修正)"] B2["外部符号: 实际地址(Bind绑定)"] end A1 -->|"Rebase"| B1 A2 -->|"Bind"| B2 问题分析 以下因素会增加Rebase/Bind的工作量: 因素 影响 ObjC类数量 每个类都有元数据需要修正 Swift类数量 Swift类在Apple平台也会生成ObjC兼容的元数据,同样需要修正 ObjC方法数量 方法列表中的指针需要修正 Category数量 Category的方法列表需要绑定 C++虚函数 虚函数表中的指针需要绑定 全局变量 指向其他符号的全局变量需要绑定 关于缓存机制: 系统动态库:Rebase/Bind 操作已在 dyld shared cache 中预先完成,不会在 App 启动时执行 App 动态库:dyld 3 的 Launch Closure 会缓存 Rebase/Bind 的元数据信息(哪些指针需要修正、修正方式等),但 Rebase/Bind 操作本身仍需每次启动时执行,因为 ASLR slide 每次启动都不同 因此,减少 ObjC 类、Category、C++ 虚函数等仍然是有效的优化手段。 ...

May 2, 2026

启动优化-Initializers

Initializers是指在main函数之前执行的初始化代码,包括C++静态构造函数和__attribute__((constructor))标记的函数。这些代码会在Pre-main阶段同步执行,阻塞启动。 问题分析 以下代码会在main之前执行: 1. C++静态构造函数 __attribute__((constructor)) static void MyInitializer() { // 耗时初始化 initializeHeavyResource(); } 2. 全局静态变量的构造函数 // 全局静态变量 static std::vector<std::string> globalCache = loadFromDisk(); // 构造函数在main前调用 3. 非基本类型的全局变量 // 非POD类型的全局变量会触发构造函数 static std::string globalString = "Hello"; static MyClass globalObject; 优化方案 方案1:延迟初始化 将全局变量改为懒加载模式: // 优化前:全局变量在main前初始化 static std::vector<std::string> globalCache = loadFromDisk(); // 优化后(纯C++方案,适用于 .cpp 文件): #include <mutex> static std::vector<std::string>* getGlobalCache() { static std::vector<std::string>* cache = nullptr; static std::once_flag onceFlag; std::call_once(onceFlag, [&] { cache = new std::vector<std::string>(); *cache = loadFromDisk(); }); return cache; } // 优化后(ObjC++方案,适用于 .mm 文件): static std::vector<std::string>* getGlobalCache() { static std::vector<std::string>* cache = nullptr; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cache = new std::vector<std::string>(); *cache = loadFromDisk(); }); return cache; } 方案2:使用Swift懒加载 Swift的懒加载特性可以避免在启动时初始化: ...

May 2, 2026