测量启动过程的耗时
当按下home键的时候,App进程并不会马上被干掉,还会在后台存活一定时间。在这个时间内如果再次回到App那么几乎不需要做什么,就可以还原到退出前的状态。这种持续存活的情况下启动App,我们称为热启动,相对而言冷启动就是App被kill掉以后一切从头开始启动的过程。
测量main()函数之前的启动时间
苹果已经提供了这个测量方式
在Xcode的菜单中选择Project→Scheme→Edit Scheme
,然后找到 Run → Environment Variables
,为项目添加环境变量DYLD_PRINT_STATISTICS
值为YES
这样Xcode在启动App后会在控制台输出启动耗时
main()函数之后
main()函数开始至applicationWillFinishLaunching结束,我们统一称为main()函数之后的部分。
虚拟内存和物理内存
早期计算机没有虚拟内存这个问题,所有的地址都是实实在在的物理地址。当应用加载到内存中时,是全部加载到内中。内存的访问都是直接访问物理内存地址,这样极其不安全。然后就是很多时候一个应用真正只是用到少量的内存,这样就存在内存的浪费。因此出现了虚拟内存,让每个应用在逻辑上存在一大片连续的虚拟内存,每个进程的虚拟内存对应一个映射表,映射到实际的物理内存上。
那么cpu在访问这个进程的时候,先通过虚拟地址寻址,然后转换为对应的物理地址(地址翻译,这需要对应的硬件--cpu的内存管理单元mmu与操作系统配合)。
也就是说,进程中使用的地址是一片单独连续的虚拟地址,通过进程映射表(页表)映射到物理内存中,这时候进程在物理内存上占用的空间不一定是连续的。这样就解决了安全和物理内存使用率的问题。
内存分页
进程有自己的虚拟内存,但是物理内存实际也没有增大,这时候要解决内存的使用效率。
进程自己对应的映射表是以页为单位,在macOS上是以4KB一页为单位,iOS是以16KB为单位。(终端输入 $PAGESIZE
可以查看到macOS的分页大小)。
如何解决内存浪费的?
应用程序加载到内存中时,并不会全部加载到物理内存中,属于懒加载,用哪一部分就加载那一部分。当访问进程的内存地址时,首先看页表,查看所要访问的对应页表是否已经加载到内存中。如果这一页没有在物理内存中时,操作系统会阻塞当前进程,发出一个缺页异常/缺页中断(pagefault
),让后将磁盘中对应页的数据加载到内存中,完成虚拟内存和物理内存的映射。
当前进程的页表数据加载到物理内存中时,不一定是连续的,也有可能会覆盖其他进程的不活跃页,这样的按需分配,极大提高内存的使用效率。
虚拟内存的安全问题
虚拟内存通过页表映射到物理内存上,因此直接访问物理地址并不能实际正确的拿到进程的数据,但是进程的虚拟内存地址相对于自己来说也是绝对的,不管程序运行多少次,如果访问同一个函数,它在虚拟内存中的地址都是一样的这样也存在安全问题(比如直接静态注入)。
这样也出现了新的技术--ASLR
(Address Space Layout Randomization)。
每次虚拟内存在加载之前,都加一个随机偏移值。iOS好像是从4.3版本开始了。
二进制重排
缺页中断/缺页异常:内存分页管理,每一页加载的时候都会发生。
在iOS中,在加载缺页内存的时候,不仅发生缺页阻塞从磁盘中加载数据,还要对加载的这页做签名的验证。
在使用中,我们一般感受不到这个过程,但是在启动中,这个过程也许你能很好的体会过。启动时,程序有大量的代码需要加载、执行,那么这个缺页中断有可能就很明显了。
如何优化?
假如我的app需要加载10页内存,但是启动的时候需要加载的代码放在1、3、5页。这时候来看看,代码在mac-o文件中的位置是根据文件加载生成的顺序来决定。那么这时候app启动需要运行的代码放在3个虚拟内存页中就需要出现3次pagefault。
如果我们那需要启动时用的代码全部放在前面1-2页中,甚至如果代码足够小只需要1页就够了,这样极大减少进程的阻塞。这也就是二进制重排。
查看pagefault
Xcode提供相关的调试工具,使用自带的instruments查看MainThread中虚拟内存的file backed page in
项目,它代表着启动时,产生的pagefault
次数。
二进制重排的优化是发生在编译链接阶段,对即将生成的二进制可执行文件进行重排。
xcode使用的连接器叫ld
它可以指向一个order_file
文件,在这个文件中指定排列符号,那么Xcode在编译时会按照指定的排列编译出可执行的文件,苹果自己也是这么用的。
开始做优化
首先看看我们项目的link map
文件,在项目的build settings
中开启这个文件的输出
然后重新编译一下,然后可以在工程的build目录里面找到一份
link map
文件
这个文件里面就记录一些链接.o的文件、mac-o文件里的一些信息、符号信息symbols
等等…
注意,这个symbols
就是关注的要点:默认情况下 它是按照文件排列顺序链接了。
通过order
文件重新排列加载顺序:
在工程配置中,添加一个指定符号顺序的order
文件后,让编译器按照指定的顺序重新排列二进制文件,把最需要加载的代码段放在内存页靠前的位置。
这里只是演示了让viewcontroller
中的几个自定义方法优先靠排列在内存分页中,实际中一个app启动时的pagefault可能多达几千次,那么需要重排的函数远不止这一点。