1、pre-main
iOS应用的启动时间分为main函数之前与main函数之后,main之前的部分叫做 pre-main
pre-main耗时情况
在iOS15之前,我们可以通过设置 Edit Scheme 中的设置 Environment Variables 增加 DYLD_PRINT_STATISTICS 来在控制台打印pre-main过程的耗时情况
Total pre-main time: 1.8 seconds (100.0%)
dylib loading time: 526.41 milliseconds (28.1%)
rebase/binding time: 165.85 milliseconds (8.8%)
ObjC setup time: 324.80 milliseconds (17.3%)
initializer time: 853.94 milliseconds (45.6%)
slowest intializers :
libSystem.B.dylib : 10.44 milliseconds (0.5%)
libMainThreadChecker.dylib : 58.23 milliseconds (3.1%)
libglInterpose.dylib : 318.94 milliseconds (17.0%)
AFNetworking : 39.55 milliseconds (2.1%)
NELivePlayerFramework : 62.94 milliseconds (3.3%)
XXXXX : 369.68 milliseconds (19.7%)
复制代码
名称 | 作用 | 说明 |
---|---|---|
dylib loading | 动态库的载入 | 动态库的载入存在耗时;动态库会存在依赖关系;系统动态库存在于共享缓存(自定义动态库不是) |
rebase/binding | 重定位符号和符号绑定 | rebase :ASLR+偏移地址;binding :将 外部符号 与其 来源库中的实现地址 进行绑定的过程 |
ObjC setup | 注册 OC 类 | 应用启动时,系统会生成 类 和 分类 的两张表,它们会注册并插入到这两张表中,产生耗时 |
initializer | 执行load 以及C++ 构造函数 |
|
slowest intializers | 列举出几个比较耗时的动态库 | libSystem、AFNetWorking等库 |
优化建议
-
苹果官方建议 自定义动态库 不超过
6
个,超过可进行多个动态库合并(需要源码支持,所以不能合并三方SDK) -
尽可能使用
initialize
方法代替+load
方法 -
集成
fui
控件可以帮助查找工程中未使用的类 -
未使用图片检测工具,拿掉未使用的图片能减小APP打包体积
2、虚拟内存
2.1、物理内存的弊端
- 可以跨进程访问,数据不安全
- 将整个程序加载到内存,导致内存浪费
2.2、虚拟内存的特点
-
在 iOS 系统中,将程序分页处理,一页为
16KB
(假设不设置分页,有100M空间,程序A占30M,程序B占40M,程序C占50M,那么A、B运行后再运行C,剩余30M不够C使用,就需要将A或B整个干掉一个腾出空间) -
虚拟地址和物理地址的映射表,也称之为页表,页表存储在内存中
-
一个进程中,只有部分功能是活跃的,所以只需要将进程中活跃的部分放入物理内存,避免物理内存的浪费
-
iOS
系统中,当进程被加载时,虚拟内存中会开辟4G
的空间(假空间),用于存放MachO
、堆区、栈区。但物理内存中,并未真的分配;当数据加载到页表中,系统会配合CPU
进行地址翻译,然后载入到物理内存中;地址翻译的过程,由CPU
上的内存管理单元(MMU
)完成
2.2.1、缺页中断(Page Fault
)
-
当程序 访问未被缓存的内存页时(功能未使用而未被载入物理内存),就会触发缺页中断
-
缺页中断会将当前进程阻塞掉,此时需要先将未被缓存的虚拟页载入到物理内存,然后再寻址,进行读取
2.2.2、页面置换
- 物理内存的空间是有限的,当内存中没有空间时,操作系统会从选择合适的物理内存页驱逐回 磁盘,为新的内存页让出位置,选择待驱逐页的过程在操作系统中叫做页面置换
2.2.3、ASLR
- 防止虚拟内存从0开始读取数据不安全而加的随机偏移量
3、二进制重排
- 在冷启动过程中,数据都还没有加载进内存,因此会产生大量的缺页中断(Page Fault)
- 在 Xcode 菜单中,选择
Product
-->Profile
-->Instruments
-->
运行测试项目,当第一个界面出来后即可停止,搜索main thread
小测试项目,启动时缺页中断 564 次,耗时 200 毫秒,如果是大型项目,会消耗更多的时间
3.1、查看代码执行顺序
-
首先将 Build Setting 中的
Write Link Map File
改为 YES -
编译后在工程的
Build
目录下,找到LinkMap
文件 -
LinkMap 文件,保存了项目在编译链接时的符号顺序,以方法/函数为单位排列
-
文件编译顺序是 Xcode 中 Build Phases -->
Compile Sources
的文件排列顺序 -
文件中方法/函数的符号顺序,就是代码的书写顺序
3.2、二进制重排原理
- 默认情况,在应用启动时,会加载大量与启动时无关的代码,导致
Page Fault
的次数增长,影响启动时间;若我们 将启动时需要的方法/函数排列在最前面,就能大大降低 Page Fault 次数,从而提升应用的启动速度
3.3、二进制重排操作
-
在项目根目录创建
.order
文件(这里随便起了个名叫pagefault.order) -
按自己想要的顺序写入启动是需要的方法/函数
-
将 .order 文件配置进工程:Build Setting -->
Order File
--> 填入./pagefault.order
-
再编译后打开 LinkMap 文件查看
可以看到按 .order 文件书写的 方法/函数 顺序,对应的 方法/函数 被提到最前边加载进内存,因此只要找的启动时需要的 方法/函数 越准确越全面,缺页中断 Page Fault 的机率就越低
总结
-
启动优化方法
- 减少自定义动态库个数
- 减少项目中未使用的类和方法
- 使用二进制重排将启动时需要的符号方法提前加载到内存
-
rebase / binding
- rebase:将虚拟内存因ASLR而产生的误差修复,
将指针指向正确的物理地址
,借助 MMU 翻译读取物理内存中的数据 - binding:将
外部符号
与它所在的库中具体实现该符号功能的地址
进行关联绑定
- rebase:将虚拟内存因ASLR而产生的误差修复,
-
确定程序启动时需要调用哪些符号需要借助
Clang插桩