前言
本文开始将对APP启动优化进行一个简单的探索,本文先介绍下启动相关的基本概念。
一: 虚拟内存 & 物理内存
物理内存时代内存地址都是物理地址,数据的访问是直接通过物理内存地址访问的。这种方式会有两个问题:
-
内存不够用。
-
内存数据不安全。
针对上面的问题,科学家们提出了虚拟内存的设想,并通过虚拟地址和物理地址对应关系的映射表让虚拟内存的设想真正落地。解决了应用程序,数据,堆栈的总的大小受限于物理内存大小的限制。每个进程都有自己独立的虚拟内存,无法互相访问,也就保证了进程间数据的安全性。
-
每个进程都有自己独立的
虚拟内存,iOS目前每个进程虚拟内存大小为4G,32位系统寻址空间从0x00000000到0xffffffff(4G),64位系统因为要兼容32位,所以前面4G空间预留了,寻址空间从0x100000000到0xffffffffffffffff(8G)。虚拟内存以page(页)为单位进行管理(每页的容量,arm64为16K,x86_64为4K)。 -
操作系统通过选择,决定将各个时刻各个进程的
活跃内容保留在物理内存中,避免物理内存的浪费。 -
当需要访问内存数据时,程序访问的始终是自己的虚拟内存,然后通过
MMU(Memory Management Unit、内存管理单元)将虚拟地址映射为物理地址。若在页表中发现所要访问的页面不在内存中,则产生缺页异常(Page Fault,或缺页中断),当前进程将会阻塞,如果操作系统物理内存中有空闲页面,就将需要访问的页面载入内存,如果没有,就采用页面置换算法用需要访问的页面替换掉不怎么活跃的页面。
虚拟内存与物理内存映射关系:
二:ASLR
上面虚拟内存解决了进程间数据安全的问题,但是由于每个进程的虚拟内存的起始地址和大小都是固定的,这就导致我们的数据非常容易被破解,相对应的数据安全又不存在了。为了解决这个问题,Apple在iOS4.3内导入了ASLR。
ASLR:地址空间配置随机加载(Address space layout randomization),是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种安全技术。
其目的是通过利用随机方式配置数据地址空间,使某些敏感数据(例如APP登录注册、支付相关代码)配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击。
由于ASLR的存在,导致可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要在启动时来修复镜像中的资源指针来指向正确的地址。即正确的内存地址 = ASLR地址 + 偏移值(offset)。
三: 启动分类
启动的过程一般是指从用户点击app图标开始到application:didFinishLaunchingWithOptions:方法执行完成为止,根据场景的不同,启动可以分为两种:冷启动和热启动。
-
冷启动:系统里没有任何进程的缓存信息。典型的是重启手机后直接启动
APP。 -
热启动:如果把
APP进程杀了,然后立刻重新启动,这次启动就是热启动,因为进程缓存还在。
通过上面分类可知,需要进行优化的是冷启动。
四: 启动阶段
iOS的APP启动以main函数为分隔点分为两个阶段:
-
T1:
main函数之前,即pre-main阶段。操作系统加载App可执行文件到内存,执行一系列的加载&绑定等工作,简单来说,就是dyld加载过程。 -
T2:
main函数之后的阶段。即从main函数开始,到application:didFinishLaunchingWithOptions:方法执行完成为止,主要是构建第一个界面,并完成渲染。
五: 优化方案
5.1: pre-main阶段的优化方案
pre-main阶段的耗时其实就是dyld加载过程的耗时,在前文iOS底层原理之dyld应用程序加载中已经分析了dyld的加载流程。
针对pre-main阶段的耗时,苹果提供了内建的测量方法,在Edit Scheme -> Run -> Arguments -> Environment Variables添加DYLD_PRINT_STATISTICS的环境变量:
运行测试项目,会看到如下输出(空项目,所以耗时较少):
-
dylib loading time:动态库载入耗时。- 优化方案:苹果系统的动态库都已经载入共享缓存,做了高速的优化。开发者自己的动态库苹果推荐不要超过
6个,如果超过了建议多个动态库合并。
- 优化方案:苹果系统的动态库都已经载入共享缓存,做了高速的优化。开发者自己的动态库苹果推荐不要超过
-
rebase/binding time:重定位(偏移修正)/绑定耗时。-
rebase(重定位):任何APP生成的二进制文件,在二进制文件内部所有的代码(如方法、函数等)都会有一个地址,这个地址是在当前二进制文件中的偏移地址。APP启动时,系统每次都会随机分配一个ASLR(Address Space Layout Randomization,地址空间布局随机化)地址值(一种安全机制,随机分配一个数值,插入二进制文件的开头)。例如,二进制文件中有一个test方法,偏移值是0x0008,随机分配的ASLR是0x1ea0,那么运行时test方法的真实内存地址就为ASLR + 偏移值(即0x0008+0x1ea0= 0x1ea8)。 -
binding(绑定):例如在前文iOS底层原理之LLVM & Clang2.1小节中介绍过的printf外部函数,在编译时生成的Mach-O文件中,会创建一个相应的符号,然后在运行时调用的时候,会将外部函数的真实地址与符号进行绑定(即动态符号绑定)。一句话概括:绑定就是给符号赋值的过程。- 优化方案:无。
-
-
ObjC setup time:OC类注册耗时。- 优化方案:移除项目中废弃的类文件和方法。如果是
Swift,尽量使用struct。
- 优化方案:移除项目中废弃的类文件和方法。如果是
-
initializer time:执行+load方法、构造函数耗时。- 优化方案:尽量不要自己实现
+load方法和+initialize方法。必须实现的情况下,就不要在这些方法里执行耗时操作或者将耗时操作放到子线程。
- 优化方案:尽量不要自己实现
5.2: main函数之后阶段的优化
pre-main阶段的优化大都解决的是资源浪费的问题,都只能达到毫秒级的优化效果而已。启动时对于用户感知最明显的,真正需要做的,大多是main函数之后的业务逻辑的优化。
在application:didFinishLaunchingWithOptions:中的业务主要分为三种类型:
- 初始化第三方
SDK。 APP运行环境配置,工具类的初始化等。- 首页呈现逻辑等。
main函数之后的优化方案:
-
减少启动初始化的流程。能懒加载的懒加载,能延迟的延迟,能放后台初始化的放后台,尽量不要占用主线程的启动时间。 -
优化代码逻辑,
去除非必须的代码逻辑,减少每个流程的消耗时间。 -
启动阶段能
使用多线程来初始化的,就使用多线程,将CPU性能发挥到最大。 -
尽量
使用纯代码来进行UI框架的搭建,尤其是主UI框架,例如UITabBarController。尽量避免使用Xib或者SB,相比纯代码而言,这种更耗时。 -
多使用假数据或缓存,迅速呈现第一个页面。
总结
启动相关的概率和优化方案就简单介绍到这里,后续将针对pre-main阶段的二进制重排进行一个简单的探索。