iOS 启动优化
一、基础概念:App启动两大类型
- 冷启动:App进程不存在,系统重新创建进程,从0到首页渲染完成(优化重点)
- 热启动:App进程还在后台,只是被挂起,直接唤醒,耗时很短,一般不做重点优化
启动时间定义标准(苹果):点击图标 → 首页第一帧渲染完成
二、冷启动完整流程(必考)
1. Pre-main(main函数之前,dyld加载阶段)
- 系统创建进程,分配虚拟内存
dyld(动态链接器)启动,加载App可执行文件Mach-O- Rebase:修正ASLR随机偏移,修复指针地址
- Bind:符号绑定,外部符号(系统库、动态库)地址填充
- 加载所有依赖dylib(系统库+自定义动态库)
- 执行
+load方法、C++全局构造函数、attribute((constructor))函数 - 执行main函数
Pre-main阶段耗时大头:dyld加载大量动态库、过多
+load、构造函数执行。
2. Post-main(main之后,到首帧渲染)
main() → UIApplicationMain → AppDelegate → 初始化根VC → View加载、布局、渲染 → 首帧
- main:创建UIApplication、AppDelegate
- AppDelegate回调:didFinishLaunchingWithOptions(最容易写重业务代码)
- 创建window、rootViewController
- VC生命周期:loadView → viewDidLoad → viewWillAppear
- AutoLayout布局、图层渲染、图片解码 → 屏幕显示第一帧
Post-main大头:didFinishLaunching内同步业务、大量对象创建、图片解码、主线程阻塞。
三、优化方向:Pre-main 优化(main之前)
1. 减少动态库 dylib
-
尽量合并自己的多个自定义动态库,减少dylib数量,dyld加载是线性加载,库越多越慢
-
避免懒加载无用库,只保留运行必须的库
系统库不用动,重点优化业务自定义动态库。
2. 减少 +load 方法(高频考点)
+load 在dyld阶段自动调用,阻塞Pre-main
- 用
+initialize替代(类第一次使用才触发,懒加载) - 使用
__attribute__((constructor))自定义构造函数也要控制数量 - 不要在
+load里面做耗时操作:网络、文件IO、大对象初始化
3. 减少Mach-O符号量
- 移除无用代码、无用类(Dead Code剥离)
- 去掉无用Category,Category也会增加符号,拖慢bind
- 编译优化:打开Dead Code Stripping,删除未使用代码
4. 编译器选项优化
- 开启
-OsRelease优化(体积、加载速度) - 关闭不需要的调试符号
四、优化方向:Post-main 优化(main之后,业务层,工程最常用)
1. didFinishLaunchingWithOptions 减负(重中之重)
❌ 禁止在里面同步执行:
- 第三方SDK初始化(埋点、推送、统计、直播SDK)
- 本地大文件读写、数据库操作
- 大量对象初始化、复杂计算
✅ 优化方案:
-
区分【必须启动前初始化】和【可以延迟初始化】
- 必须:推送核心、Crash监控(崩溃收集要尽早)
- 其余全部丢到异步子线程或者首帧渲染后延迟执行
-
延迟调度:
dispatch_after/ 监听mainRunLoop空闲时机(CATransaction/ RunLoop空闲任务) -
SDK懒加载:用到的时候再初始化,不要启动一次性全部初始化
坑:子线程初始化SDK,但SDK内部操作UI,会crash;SDK初始化要确认线程安全。
2. 首页VC优化
- viewDidLoad / viewWillAppear 不要做重任务
- 图片延迟加载:大图不要一次性全部解码,放到子线程解码,首屏只加载可视区域图片
- 减少首屏控件数量,减少AutoLayout约束,复杂布局用frame降低布局开销
- 避免在主线程做数据库查询、大量计算
五、其他优化点
-
二进制重排(非常重要,大厂标配)
- 原理:PageFault缺页中断。启动用到的函数分散在Mach-O不同页,触发大量缺页中断,读磁盘慢。
- 二进制重排:把启动阶段调用的函数全部排列到Mach-O文件前面连续内存页,减少PageFault,降低IO耗时。
- 工具:Clang SanitizerCoverage收集启动函数trace,生成order文件。
-
资源优化
- 图片:压缩图片、使用WebP,大图按需加载,避免启动一次性加载大量图片
- 减少启动阶段plist、json大文件读取
- 无用资源删除(Asset、本地化资源)
-
冷启动监控(衡量优化效果)
-
系统工具:Instruments → App Launch 专门看启动耗时,区分Pre-main、Post-main
-
埋点:自己埋点记录
main开始、首帧渲染完成时间,线上采集真实用户数据Instruments的App Launch模板会自动拆分dyld各个阶段耗时,直接看瓶颈。
六、面试高频追问
Q:+load 和 +initialize区别?
+load:app加载类的时候就调用,dyld阶段,不管类有没有使用;父类先于子类,类先于category;阻塞启动。+initialize:类第一次收到消息时懒加载调用,只调用一次,懒触发,不阻塞Pre-main。
Q:二进制重排原理是什么?PageFault?
操作系统内存分页,程序不会一次性全部加载进内存;访问不在内存的函数时触发PageFault,磁盘读入内存,IO很慢。二进制重排让启动需要的代码集中在连续页面,减少缺页次数,减少磁盘IO。
Q:didFinishLaunching放子线程有什么坑?
- UI操作不能放子线程;
- 部分SDK内部依赖主线程RunLoop;
- 任务如果依赖顺序,要处理依赖关系;
- 任务执行完前用户操作页面,注意空状态保护。
Q:冷启动优化优先级排序(背这个)
- 二进制重排(PageFault,收益很大)
- Pre-main:减少动态库、干掉无用+load
- Post-main:didFinishLaunching减负,非必要SDK延迟初始化
- 首页VC、图片、布局优化
- 资源瘦身
七、简单一句话总结面试口述版
iOS冷启动分为Pre-main和Post-main两个阶段。Pre-main是dyld加载Mach-O、绑定符号、执行load;Post-main是main函数到首帧渲染。优化优先减少动态库、移除+load、二进制重排减少缺页中断;业务侧把didFinishLaunching里面非必要任务异步延迟,首页减少主线程计算和图片解码,最后用Instruments App Launch模板定位瓶颈。
图片优化
区分两套东西:OpenGL ES / Metal
-
OpenGL ES(老 API,PowerVR SGX,iPhone4S/5) MAX_TEXTURE_SIZE = 4096 × 4096,超过这个尺寸创建纹理直接失败,无法上传 GPU。 iPhone4 及更早:2048×2048 上限。
-
Metal(A7 芯片及以后,iPhone5s+)
- A7~A10:最大纹理 8192 × 8192
- A11 及更新(iPhone X 以后):16384 ×16384
二进制重排
待定,目前没时间。