iOS之启动优化
一、APP的启动(分为两个阶段)
1.1 pre-main
pre-main在main()函数之前,可分为四个阶段:
1.dylibloading:加载动态库。可以通过减少动态库的数量来优化这一部分所消耗的时间。
苹果的建议是一个项目里面自己制作的动态库的数量不超过6个。 2. ObjCsetup:注册Objc类,进行selector唯一性检测等。可以通过减少Objc类的数量,减 少selector的数量来进行优化。 3. rebase/binding:rebase指针修复,binding符号绑定。这一步的优化手段和第2步一样。 4. initializer:各种初始化的操作,比如执行objc的+load函数,C++的构造函数等。
不要在+load函数里面做一些耗时的操作,或者把一些操作延时的放在+initialize里面去执行。
1.2 main函数
从main()函数开始到执行完appDelegate的didFinishLaunchingWithOptions方法展示首⻚数据。
1. 少使用xib和storyboard。 2. 删除NSLog打印。 3. 整理didFinishLaunchingWithOptions方法里面的业务逻辑,可以异步请求的异步请求,可 以延时加载的延时加载。
二、以一个APP来实际观察一个程序的耗时情况(https://juejin.cn/post/7109861095405256735)
在iOS15之前,我们可以通过设置 Edit Scheme 中的设置 Environment Variables 增加 DYLD_PRINT_STATISTICS 来在控制台打印pre-main过程的耗时情况。
三、从上面的描述中我们可以得到一些优化的建议
1.苹果官方建议 自定义动态库 不超过6个,超过可进行多个动态库合并(需要源码支持,所以不能合并三方SDK) 2.尽可能使用initialize方法代替+load方法 3.集成fui控件可以帮助查找工程中未使用的类(减少未使用类的加载) 4.通过未使用图片检测工具来检测未使用的图片,减少未使用的图片能减小APP打包体积
四、虚拟内存
4.1 物理内存
在之前并没有虚拟内存的时候,程序的加载都是直接被加入到物理内存中的。这样会有一些缺点:
1.可以跨进程访问,数据不安全 2.将整个程序加载到内存,导致内存浪费
4.2 虚拟内存
后来出现了虚拟内存,实现物理内存和虚拟内存的映射,也称之为页表,页表存储在内存中。
- 在 iOS 系统中,将程序分页处理,一页为
16KB
(假设不设置分页,有100M空间,程序A占30M,程序B占40M,程序C占50M,那么A、B运行后再运行C,剩余30M不够C使用,就需要将A或B整个干掉一个腾出空间) - 虚拟地址和物理地址的映射表,也称之为页表,页表存储在内存中
- 一个进程中,只有部分功能是活跃的,所以只需要将进程中活跃的部分放入物理内存,避免物理内存的浪费
iOS
系统中,当进程被加载时,虚拟内存中会开辟4G
的空间(假空间),用于存放MachO
、堆区、栈区。但物理内存中,并未真的分配;当数据加载到页表中,系统会配合CPU
进行地址翻译,然后载入到物理内存中;地址翻译的过程由CPU
上的内存管理单元(MMU
)完成。
4.2.1 缺页中断(Page Fault)
1.当程序访问未被缓存的内存页时(功能未使用而未被载入物理内存),就会触发缺页中断
2.缺页中断会将当前进程阻塞掉,此时需要先将未被缓存的虚拟页载入到物理内存,然后再寻址,进行读取
4.2.2 页面置换
物理内存的空间是有限的,当内存中没有空间时,操作系统会从选择合适的物理内存页驱逐回 磁盘,为新的内存页让出位置,
选择待驱逐页的过程在操作系统中叫做页面置换。
4.2.3 地址布局随机化(ASLR:address space layout randomization)
地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,
通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。
使得可执行文件和动态库在虚拟内存中的地址在每次启动都不固定。
五、二进制重排
在APP冷启动过程中,因为数据都还没有加载进内存,因此会产生大量的缺页中断,加长页面启动的时间。我们可以通过二进制重排来减少缺页中断的产生,从而加快页面的启动时间。
5.1 看一下微信APP的启动时间
我们在Xcode->Product->Profile->Instruments可以找到一个System Trace的工具:
然后我们运行项目,当第一个界面出来的时候立即停止,搜索main thread;然后我们可以带伞启动时产生了缺页中断3735次,耗时516ms。
5.2 查看代码的执行顺序
首先将 Build Setting 中的Write Link Map File
改为 YES;然后在编译后在过程的Build目录下,找到LinkMap文件,这里保存了项目在编译链接时产生的符号顺序,以方法和函数为单位排列:
文件的编译顺序是按照Xcode->Build Phases->Compile Source中文件的排列顺序进行排列的,而文件中方法或者是函数的顺序是按照代码的的书写顺序排列的。
5.3 二进制重排可以提升APP启动速度的原因
默认情况,在应用启动时,会加载大量与启动时无关的代码,导致Page Fault的次数增长,影响启动时间;
若我们将启动时需要的方法/函数排列在最前面(确保在同一个页面),就能大大降低 Page Fault 次数,从而提升应用的启动速度
5.4 二进制重排的具体操作
1. 在项目根目录创建.order
文件(这里随便起了个名叫lg.order)
2. 按照启动的函数顺序依次写入这个order文件中,如果是不存在的函数则默认不执行
3.将 .order 文件配置进工程:Build Setting --> Order File
--> 填入./lg.order
4. 再编译后打开 LinkMap 文件查看;可以看到按 .order 文件书写的 方法/函数 顺序,对应的 方法/函数 被提到最前边加载进内存,因此只要找的启动时需要的 方法/函数 越准确越全面,缺页中断 Page Fault 的机率就越低
六、补充知识
//rebase、binding rebase:将虚拟内存因ASLR而产生的误差修复,将指针指向正确的物理地址,借助MMU翻译读取物理内存中的数据 binding:将外部符号与它所在的库中具体实现该符号功能的地址进行关联绑定
确定程序启动时需要调用哪些符号需要借助Clang插桩(下一节会说)