iOS 启动优化是提升用户体验的核心环节,直接影响用户对 App 的第一印象。启动过程分为 冷启动(App 进程未运行,需从头加载)和 热启动(App 进程在后台,重新唤醒),其中冷启动优化是重点。以下从启动流程拆解、各阶段优化措施、工具链辅助三个维度详细说明。
一、启动流程拆解
iOS 冷启动可分为 pre-main 阶段(从用户点击图标到 main() 函数执行前)和 main 阶段(从 main() 到首屏渲染完成),各阶段耗时需分别优化。
1. pre-main 阶段(系统主导)
从进程创建到 main() 被调用前,主要做以下工作(按顺序):
• 加载可执行文件:加载 App 的二进制文件(Mach-O)到内存。
• 加载动态库(dylibs):加载 App 依赖的所有动态库(系统库如 UIKit、自定义动态库等)。
• Rebase & Binding:修正二进制中的指针偏移(Rebase),绑定符号到实际内存地址(Binding)。
• ObjC Runtime 初始化:注册所有类、分类,初始化 objc_runtime 环境。
• 初始化器执行:执行 +load 方法、C++ 静态构造函数、attribute((constructor)) 修饰的函数等。
2. main 阶段(应用主导)
从 main() 函数执行到 首屏渲染完成(UIWindow 的 rootViewController 完成布局和绘制),主要工作:
• 执行 main() 函数。
• 初始化 UIApplication、AppDelegate。
• 执行 application:didFinishLaunchingWithOptions: 中的逻辑(如初始化第三方库、配置环境等)。
• 首屏视图控制器的创建、布局(viewDidLoad、viewWillAppear、layoutSubviews)和渲染。
二、pre-main 阶段优化(系统级优化)
pre-main 阶段由系统主导,优化核心是 减少耗时操作的数量和复杂度,可通过 Xcode 日志(Edit Scheme -> Run -> Arguments -> Environment Variables 中添加 DYLD_PRINT_STATISTICS=1)打印各步骤耗时:
Total pre-main time: 237.36 milliseconds
dylib loading time: 112.51 milliseconds
rebase/binding time: 19.28 milliseconds
ObjC setup time: 45.32 milliseconds
initializer time: 60.25 milliseconds
1. 优化动态库加载(dylib loading)
动态库(包括系统库、自定义库)加载是 pre-main 耗时大户,每加载一个动态库都需要解析、验证签名、建立依赖关系。
• 合并自定义动态库:将多个功能相关的自定义动态库(.framework 或 .dylib)合并为一个,减少动态库数量(建议控制在 60 个以内)。
• 优先使用静态库:静态库(.a 或静态 .framework)在编译时直接链接到二进制,无需启动时加载,适合不共享的内部库。
• 避免依赖无用系统库:移除 Link Binary With Libraries 中未使用的系统库(如未用 MapKit 就删除,系统库虽预加载,但多余依赖会增加解析成本)。
2. 优化 Rebase & Binding
• Rebase:修正二进制中指针的偏移(因 ASLR 随机地址导致),耗时与二进制中“指针数量”正相关。
• Binding:将符号(如函数、变量)绑定到实际内存地址,耗时与“符号数量”正相关。
优化措施:
• 减少二进制体积:删除无用代码(Dead Code)、资源,通过 Strip Linked Product 去除调试符号,减少指针和符号数量。
• 启用链接时优化(LTO):在 Build Settings -> Link-Time Optimization 设为 Incremental 或 Whole Module,编译器会在链接时合并重复符号、优化代码,减少符号数量。
3. 优化 ObjC Runtime 初始化(ObjC setup)
Runtime 需注册所有类(Class)、分类(Category),并建立方法列表,耗时与“类/分类数量”正相关。
• 删除无用类和分类:通过 clang -rewrite-objc 分析生成的 .cpp 文件,或使用工具(如 FUI)检测未使用的类/分类,直接删除。
• 避免滥用分类:分类会增加 Runtime 注册成本,非必要时用继承或组合替代。
4. 优化初始化器(initializers)
+load 方法、C++ 静态构造函数(static ClassName obj;)、__attribute__((constructor)) 函数会在 pre-main 阶段同步执行,阻塞启动流程。
• 禁用 +load 方法:+load 会在类加载时强制同步执行,改用 +initialize(懒加载,首次使用类时调用),或延迟到 main 阶段通过 dispatch_once 初始化。
// 替代 +load 的方式
+ (void)initialize {
if (self == [MyClass class]) { // 避免子类调用
[self setup];
}
}
• 移除 C++ 静态构造函数:用 OC 代码替代 C++ 初始化逻辑,或延迟到 main 阶段初始化。
• 检查第三方库:部分第三方库(如统计、监控)会在 +load 中做初始化,可尝试替换为“手动初始化”版本,或在 main 阶段手动调用。
三、main 阶段优化(应用级优化)
main 阶段的核心是 减少 didFinishLaunching 中的阻塞操作,让首屏尽快渲染。
1. 延迟初始化非首屏任务
将非首屏必要的初始化(如日志上报、配置同步、非核心第三方库)延迟到首屏渲染完成后。
• 分阶段初始化:
首屏必要任务(如用户登录状态校验、路由初始化)在 didFinishLaunching 同步执行。
非首屏任务(如推送注册、数据分析)用 dispatch_after 或 DispatchQueue.global().async 延迟执行。
// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 1. 首屏必要初始化(同步)
[self setupRootViewController];
[self setupNetworkConfig];
// 2. 非首屏任务(延迟到首屏渲染后)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self setupPushService]; // 推送
[self setupAnalytics]; // 统计
});
return YES;
}
2. 优化首屏渲染
首屏渲染耗时直接影响用户感知,需减少主线程阻塞:
• 简化首屏布局:
避免首屏使用复杂层级的 UIView 或 AutoLayout(AutoLayout 复杂约束计算耗时),优先用 Frame 布局。
首屏图片优先使用本地缓存,避免网络加载;提前解码图片(UIImage 首次渲染时会解码,耗时),用 CGImageSourceCreateWithData 预解码。
• 延迟首屏非必要视图:
首屏仅加载“用户可见区域”的视图,滚动区域外的视图(如列表底部 cell)通过 UITableView/UICollectionView 的复用机制延迟加载。
用 UIStackView 时避免嵌套过深,复杂布局可拆分为多个独立视图。
3. 优化第三方库初始化
第三方库(如地图、支付、监控)是 main 阶段耗时的常见来源:
• 按需初始化:非首屏依赖的库(如“我的页面”的分享功能)在用户首次进入对应页面时初始化,而非启动时。
• 替换轻量库:如用自研网络库替代体积庞大的第三方库,或裁剪第三方库的无用功能(通过 cocoapods 的 subspec 只引入必要模块)。
4. 避免主线程耗时操作
didFinishLaunching、viewDidLoad、viewWillAppear 等方法中的耗时操作(如数据库读写、大文件解析、复杂计算)会阻塞主线程,导致启动卡顿:
• 移到子线程:将耗时操作(如 JSON 解析、数据库查询)放到全局队列,完成后再回调主线程更新 UI。
// 错误:主线程解析大 JSON
NSDictionary *data = [NSJSONSerialization JSONObjectWithData:largeData options:0 error:&error];
// 正确:子线程解析
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSDictionary *data = [NSJSONSerialization JSONObjectWithData:largeData options:0 error:&error];
dispatch_async(dispatch_get_main_queue(), ^{
// 主线程更新 UI
});
});
四、工具链辅助优化
通过工具定位瓶颈,避免“盲目优化”:
1. 测量启动时间
• Xcode 日志:添加 DYLD_PRINT_STATISTICS_DETAILS=1 环境变量,打印 pre-main 阶段详细耗时(包括每个动态库的加载时间)。
• 代码埋点:在 main 函数、didFinishLaunching 开始/结束、首屏渲染完成(如 viewDidAppear)处记录时间戳,计算各阶段耗时。
// main.m
#import <mach/mach_time.h>
uint64_t startTime = mach_absolute_time();
int main(int argc, char * argv[]) {
uint64_t mainStartTime = mach_absolute_time();
// ...
}
• Instruments:
App Launch:直观展示从点击图标到首屏渲染的全流程耗时,标记各阶段(如 dylib 加载、首屏绘制)的时间占比。
Time Profiler:分析启动过程中函数的耗时,定位主线程上的耗时操作(如某个第三方库的初始化函数)。
2. 检测冗余代码/资源
• App Thinning:通过 Xcode -> Product -> Analyze App Size 分析二进制中无用的类、方法、资源,删除冗余内容。
• Link Map:在 Build Settings -> Write Link Map File 设为 YES,生成 linkmap 文件(路径在 Intermediate Build Files Path 中),分析符号占用大小,删除无用代码。
五、注意事项
1. 平衡优化与功能:避免为了极致优化牺牲功能稳定性(如过度延迟初始化导致某些功能首次使用时崩溃)。
2. 适配不同设备:低端设备(如 iPhone SE 系列)的启动瓶颈可能与高端设备不同,需针对性测试。
3. 监控线上数据:通过埋点收集用户实际启动时间(如“点击图标到首屏显示”的耗时),结合 Crash 监控(如 Firebase、Bugly)分析异常启动案例。
总结
iOS 启动优化的核心思路是:“减少阻塞、延迟非必要、聚焦首屏”。通过拆解 pre-main 和 main 阶段的耗时点,结合工具定位瓶颈,优先优化高占比任务(如动态库加载、+load 方法、主线程耗时操作),最终实现“用户点击图标后,最快看到可交互的首屏”。