背景
App启动时间是用户直观体验App的第一步,苹果建议App启动时间不要超过0.4ms,超过20s的启动时间被系统直接杀死,所以优化App启动时间是极其必要的,工欲善其事,必先利其器,要想做到深入的优化,我们需要先了解下App的启动阶段,知其然知其所以然。
如何衡量APP的启动时间是否及格
极好 | 合格 | 需要优化 |
---|---|---|
< 0.4s | < 1s | 需要优化 |
App启动流程
从几个大的阶段来划分
- 加载Mach-O阶段
- dyld 阶段
- main 之后阶段:加载启动项到最终的viewDidAppear调用加载第一帧。
从dyld和runtime角度来划分
如果结合dyld 和runtime 这两个核心模块来划分,启动阶段主要由他们两者协作完成。
步骤1: 内核态加载mach-o文件和可执行文件
Mach-O文件简介
- Mach object的缩写,是Mac、iOS上用于存储程序、库的标准格式 ,Mach-O文件是一种叫法,就像以 .text 结尾的文件,被叫做为text文件
常见的Mach-O文件有:
- MH_OBJECT:目标文件(.o)、静态库文件(.a) 静态库其实就是N个.o合并在一起
- MH_EXECUTE:可执行文件 .app/xx
- MH_DYLIB:动态库文件 .dylib 或 .framework/xx
- MH_DYLINKER:动态链接编辑器 /usr/lib/dyld
- MH_DSYM:存储着二进制文件符号信息的文件 .dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩溃信息)
可执行文件:
- 平时编写的代码最终会被编译成为一个Mach-O格式的文件
- 开发过程中所用到的动态库(比如:UIKit、Foundation) 依赖信息也会存储在可执行文件中
步骤2: dyld动态链接阶段
dyld是什么
"dyld" 是苹果操作系统中的一个重要组件,它是动态链接器(dynamic linker)的缩写。动态链接器是操作系统加载和链接可执行文件所需的共享库的核心组件之一。
dyld的功能
在程序启动时加载和链接程序所依赖的共享库,并将其映射到进程的内存空间中。它负责解析和处理共享库之间的符号依赖关系,以及处理运行时的符号重定位。
具体来说,dyld 的工作流程如下:
- 加载:当一个可执行文件(如应用程序)启动时,dyld 负责加载可执行文件和它所依赖的共享库到内存中。
- 符号解析:dyld 解析可执行文件和共享库中的符号引用,找到对应的符号定义,以便正确地链接和运行程序。
- 符号重定位:在加载和链接过程中,dyld会处理符号重定位,将程序中的符号引用指向正确的地址。
- 启动程序:完成加载和链接后,dyld 将控制权转交给程序的入口点,使其开始执行。
dyld 的存在使得应用程序可以动态地加载和链接共享库,从而实现了代码的共享和重用。这也是为什么在 iOS 开发中,我们可以使用各种系统提供的框架和库来构建应用程序。dyld 是苹果操作系统中负责动态加载和链接共享库的组件,它在应用程序启动时发挥着关键的作用,确保程序能够正确地加载和执行所需的代码和库。
这里插入一条tip 上面说的共享库是动态库,iOS目前只有系统库是真正的动态库(不是开发者制作的动态库),具备以下特征:
- 程序运行时由系统动态加载到内存,而不是复制,供程序调用。
- 系统只加载一次,多个程序共用,节省内存。因此,编译内容更小,而且因为动态库是需要时才被引用,所以更快。 简单认识:系统的UIKit框架最终被dyld以动态库的形式加载到内存 !
dyld如何工作
苹果给出如下图:
load dylibs(递归哦加载依赖共享库) > rebase(确定各个库的内存排布) bind(库之间依赖的符号绑定) > objc(Notify ObjC Runtime) > initializers(各类的初始化)
1: load dylibs 装载app的可执行文件,同时会递归加载所有依赖的动态库。
- Parse image(解析图像):在这个步骤中,dyld 解析可执行文件或共享库的二进制格式。它会读取可执行文件的头部和段(segments),以及共享库的符号表和重定位信息等。通过解析图像,dyld 能够了解文件的结构、符号引用和重定位需求。
- Map image(映射图像):在这一阶段,dyld 将可执行文件或共享库映射到进程的内存空间中。它会分配适当的内存区域,并将二进制文件的内容加载到这些内存区域中。通过映射图像,dyld 将文件中的代码、数据和资源加载到内存,为后续的重定位和符号绑定做准备。
2: Rebase + bind
- Rebase image(重定位图像):在此步骤中,dyld 处理可执行文件和共享库中的重定位信息。重定位信息描述了代码和数据的位置相对于内存中的基地址的偏移量。dyld 根据基地址和重定位信息来计算并更新代码和数据的绝对地址,以确保它们在内存中正确定位。
- Bind image(符号绑定图像):在这个阶段,dyld 解析可执行文件和共享库中的符号引用,并将它们绑定到相应的符号定义。符号绑定是将符号引用与符号定义相关联的过程,确保程序能够正确地访问和执行所需的符号。通过符号绑定,dyld 确保程序能够正确链接并执行依赖的函数和变量。
3: objc(Notify ObjC Runtime)
- mapimages 对二进制文件内容解析处理。
- runtime在此处初始化,对class和category进行注册。
- 进行各种objc结构的初始化(objc 类被定义和注册)。
- 分类被插入到方法列表中。
- selector唯一性判断。
4: Initializers
- loadimages 调用 call_load_images 加载 类和 分类的 load方法
- 调用c++静态初始化器和__attribute(construct)修饰的函数
至此可执行文件和动态库的符号sel class protocol IMP 都已经按需加载到内存中了,被runtime管理最后,Dyld calls main()
步骤3: 进入main函数
- 接下来就是 UIApplicationMain 函数,相关的调用了,Appdelegate会依次执行 对应的生命周期方法。
- 创建整个app的autoreleasepool,初始化初始window,app界面开始展。
- 指定rootviewcontroller,调用业务代码,完成各阶段业务。
- main页面viewDidAppear 完成页面第一帧渲染。至此启动完成。
优化点梳理
加载mach-o阶段
重新排列函数符号位置,降低MACH-O文件载入内存时PageFault缺页中断频率 - 二进制重排
- 一种是抖音的方案二进制重排。(官方说会有百分之30提升,自己尝试并没有太大提升。)
- 另外是苹果推出的pgo。(大概有百分之10左右的提升)
后边文章会单独拿一个专题讲解下
针对dyld阶段
- 减少动态库,合并动态库 (定期清理不必要的动态库)
- 减少oc类 分类 方法 sel(定期清理不必要的类 分类)
- 减少c++虚函数数量
- swift尽量使用struct,这样不会在进行初始化
针对objc 和 initialize 可以看做是 runtime 阶段
- 用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、ObjC的+load
针对main函数之后阶段
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
- 启动任务的顺序调整优化
- 降低初始视图的复杂性
- 按需加载
感谢您的阅读,本篇文章您已阅读完毕,欢迎浏览我的其他分享!