Main
函数之前的性能检测
应用的启动时间,一般分为Main
函数执行之前和之后,执行之前称之为pre-main
系统提供了环境变量,让开发者可以看到pre-main
过程中的耗时
查看方式:在Xcode
中,选择项目 Product
–> Scheme
–> Edit Scheme
:
就能打开设置弹框,然后在 Environment Variables
里面添加DYLD_PRINT_STATISTICS
环境变量,设置为YES
。
运行项目,就能打印出对应的耗时信息:
-
dylib loading time
:动态库的载入耗时:-
动态库的载入肯定会存在耗时,并且动态库会存在依赖关系。系统动态库存在于共享缓存,但自定义动态库没有这个待遇,所以苹果官方建议不要超过
6
个自定义动态库,超过可进行多个动态库合并,以此来优化动态库加载的耗时; -
动态库的合并,需要源码才能进行。所以我们只能合并自己开发的动态库,日常使用的三方SDK可能无法合并。
-
-
rebase/binding time
:重定位符号和符号绑定的耗时:-
rebase
:系统采用ASLR
技术,保证地址空间随机化。所以在运行时,需要通过rebase
进行重定位符号,使用ASLR
+偏移地址; -
binding
:使用外部符号,编译时无法找到函数地址。所以在运行时,dyld
加载共享缓存,加载链接动态库之后,进行binding
操作,重新绑定外部符号。
-
-
ObjC setup time
:注册OC
类的耗时:-
注册
OC
类的过程,读取二进制的data
段找到OC
的相关信息,然后注册OC
类。应用启动时,系统会生成类和分类的两张表,OC
类和分类的注册,会插入到这两张表中,所以会造成一定的时间消耗; -
这部分时间很难优化,除非减少项目中类和分类的定义;
-
减少类和所属分类
load
方法的使用,让类以懒加载的方式加载。
-
-
initializer time
:执行load
以及C++
构造函数的耗时:- 尽可能使用
initialize
方法代替load
方法。
- 尽可能使用
-
slowest intializers
:列举出几个比较耗时的动态库。
虚拟内存
概述
早期的操作系统
早期的操作系统,并没有虚拟内存的概念。系统由进程直接访问内存中的物理地址,这种方式存在严重的安全隐患。内存中的不同进程,可以计算出它们的物理地址,可以跨进程访问,可以随意进行数据的篡改。
早期的程序也比较小,在运行时,会将整个程序全部加载到内存中。但随着软件的发展,程序越来越大,而且还有大型游戏的诞生,导致内存越来越紧张。这就是早期系统中,为什么经常出现内存不足的提示。
所以,直接使用物理内存的弊端:
-
可以跨进程访问,数据不安全。
-
将整个程序加载到内存,导致内存浪费。
虚拟内存系统
现代的操作系统都引入了虚拟内存,进程持有的虚拟地址(Virtual Address
)会经过内存管理单元(Memory Mangament Unit
)的转换变成物理地址,然后再通过物理地址访问内存。
操作系统以页为单位管理内存,在iOS
系统中,一页为16KB
。所以虚拟地址和物理地址的映射表,也称之为页表。页表存储在内存中,有了页表,就可以将程序和物理内存完全阻隔开。
一个进程中,只有部分功能是活跃的,所以只需要将进程中活跃的部分放入物理内存,避免物理内存的浪费。
现代的操作系统进行了更合理的优化,例如iOS
系统中,当进程被加载时,虚拟内存中会开辟4G
的空间(假空间),用于存放MachO
、堆区、栈区。但物理内存中,并未真的分配。当数据加载到页表中,系统会配合CPU
进行地址翻译,然后载入到物理内存中。地址翻译的过程,由CPU
上的内存管理单元(MMU
)完成。
页表中记录了内存页的状态、虚拟内存和物理内存的对应关系。其中状态分为:未分配(Unallocated
)、未缓存(Uncached
)和已缓存(Cached
)
-
未分配的内存页,是没有被进程申请使用的,也就是空闲的虚拟内存,不占用虚拟内存磁盘的任何空间。
-
未缓存的内存页,仅在虚拟内存中,没有被物理内存缓存。
-
已缓存的内存页,同时存在于虚拟内存和物理内存中。
使用虚拟内存的优势:
-
程序以懒加载的方式加载到内存中,按需加载,避免内存浪费。
-
将程序和物理内存完全阻隔开,无法跨进程访问,数据更安全。
进程通信由系统提供API
,使用kernel
发送信号。但不能直接跨进程访问,保证数据的安全。
缺页中断
-
当程序访问未被缓存的内存页时,就会触发缺页中断;
-
缺页中断会将当前进程阻塞掉,此时需要先将数据载入到物理内存,然后再寻址,进行读取;
-
部分情况下,被访问的页面已经加载到物理内存中,但页表中并不存在该对应关系,这时只需要在页表中建立虚拟内存到物理内存的关系即可;
-
其他情况下,操作系统需要将磁盘上未被缓存的虚拟页加载到物理内存中。
页面置换
物理内存的空间是有限的,当内存中没有空间时,操作系统会从选择合适的物理内存页驱逐回磁盘,为新的内存页让出位置,选择待驱逐页的过程在操作系统中叫做页面置换。
例如,同一台设备上,依次打开微信、微博、淘宝、京东、抖音,此时再回到微信,又会看到微信的启动界面。因为系统在内存紧张的时候,会按照活跃度将最不活跃的内存进行覆盖;
对于微信来说,程序进程还存在于系统中,所以进行热启动;
-
冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动;
-
热启动:当启动应用时,后台已有该应用的进程(例:按
home
键回到桌面,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。
ASLR
程序的代码在不修改的情况下,每次加载到虚拟内存中的地址都是一样的,这样的方式并不安全。为了解决地址固定的问题,出现了ASLR
技术。
ASLR
(Address space layout randomization
):是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。
大部分主流的操作系统已经实现了ASLR
:
-
Linux
:在内核版本2.6.12
中添加ASLR
; -
Windows
:Windows Server 2008
、Windows 7
、Windows Vista
、Windows Server 2008 R2
,默认情况下启用ASLR
,但它仅适用于动态链接库和可执行文件; -
Mac OS X
:Apple
在Mac OS X Leopard10.5
(2007年
十月发行)中某些库导入了随机地址偏移,但其实现并没有提供ASLR
所定义的完整保护能力。而Mac OS X Lion10.7
则对所有的应用程序均提供了ASLR
支持。Apple
宣称为应用程序改善了这项技术的支持,能让32
及64位
的应用程序避开更多此类攻击。从OS X Mountain Lion10.8
开始,核心及核心扩充(kext
)与zones
在系统启动时也会随机配置; -
iOS
(iPhone
、iPod touch
、iPad
):Apple
在iOS4.3
内导入了ASLR
; -
Android
:Android 4.0
提供地址空间配置随机加载(ASLR
),以帮助保护系统和第三方应用程序免受由于内存管理问题的攻击,在Android 4.1
中加入地址无关代码(position-independent code
)的支持。
二进制重排
缺页中断的消耗
当系统访问虚拟内存时,发现数据还未加载到物理内存中,会触发缺页中断(Page Fault
),造成进程阻塞。此时系统会先将数据加载到物理内存中,进程才能继续运行。虽然每一页数据加载到内存的速度很快,毫秒级别,但在应用冷启动时,可能会出现大量的缺页中断,对启动速度带来一定的时间消耗。
使用测试项目,查看应用在启动过程中,Page Fault
所带来的消耗。
在Xcode
菜单中,选择Product
–> Profile
:
打开Instruments
:
运行测试项目,当第一个界面出来后即可停止,搜索main thread
- 一个小测试项目,启动时缺页中断
564
次,耗时200
毫秒。如果是微信、抖音等大型项目,不进行优化可达到6000
次以上,造成不小的时间消耗。
二进制重排的原理
搭建测试项目,查看代码顺序。
打开项目,在Build Settings
–> Write Link Map File
,设置为YES
:
编译项目,来到工程的Build
目录下,找到LinkMap
文件:
LinkMap
文件,保存了项目在编译链接时的符号顺序,以方法/函数为单位排列:
# Symbols:
# Address Size File Name
0x100005F80 0x0000002C [ 1] +[ViewController load]
0x100005FAC 0x00000048 [ 1] -[ViewController viewDidLoad]
0x100005FF4 0x0000007C [ 2] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100006070 0x00000100 [ 2] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x100006170 0x00000074 [ 2] -[AppDelegate application:didDiscardSceneSessions:]
0x1000061E4 0x0000009C [ 3] _main
0x100006280 0x0000009C [ 4] -[SceneDelegate scene:willConnectToSession:options:]
0x10000631C 0x0000004C [ 4] -[SceneDelegate sceneDidDisconnect:]
0x100006368 0x0000004C [ 4] -[SceneDelegate sceneDidBecomeActive:]
0x1000063B4 0x0000004C [ 4] -[SceneDelegate sceneWillResignActive:]
0x100006400 0x0000004C [ 4] -[SceneDelegate sceneWillEnterForeground:]
0x10000644C 0x0000004C [ 4] -[SceneDelegate sceneDidEnterBackground:]
0x100006498 0x00000024 [ 4] -[SceneDelegate window]
0x1000064BC 0x0000003C [ 4] -[SceneDelegate setWindow:]
0x1000064F8 0x0000003C [ 4] -[SceneDelegate .cxx_destruct]
0x100006534 0x0000000C [ 5] _NSLog
...
复制代码
文件编译顺序是Xcode
中Build Phases
–> Compile Sources
的文件排列顺序:
文件中方法/函数的符号顺序,就是代码的书写顺序:
- 以
ViewController.m
为例,load
方法在viewDidLoad
方法之前,和LinkMap
文件中的顺序一致。
所以,按照默认配置,在应用启动时,会加载到大量与启动时无关的代码,导致Page Fault
的次数增长,影响启动时间。如果可以将启动时需要的方法/函数排列在最前面,就能大大降低缺页中断的可能性,从而提升应用的启动速度,这就是二进制重排的核心原理。
二进制重排的配置
二进制重排的配置非常简单,只需要在工程中创建.order
文件,按固定格式,将启动时需要的方法/函数顺序排列,然后在Xcode
中使用.order
文件即可。通过LinkMap
文件中的顺序,查看最终的排序是否符合预期。
在工程根目录创建.order
文件
打开hk.order
文件,写入启动时需要的方法/函数:
+[ViewController load]
+[AppDelegate load]
_main
复制代码
让Xcode
使用.order
文件,在Build Setting
–> Order File
中配置:
编译项目,打开LinkMap
文件:
# Symbols:
# Address Size File Name
0x100005F54 0x0000002C [ 1] +[ViewController load]
0x100005F80 0x0000002C [ 2] +[AppDelegate load]
0x100005FAC 0x0000009C [ 3] _main
0x100006048 0x00000048 [ 1] -[ViewController viewDidLoad]
0x100006090 0x0000007C [ 2] -[AppDelegate application:didFinishLaunchingWithOptions:]
...
复制代码
- 最前面三个方法/函数,按照
.order
文件中的顺序排列。
由此可见,如果我们将项目中,启动时需要调用的所有方法/函数都找到,把它们全部写入到.order
文件中,就能大大降低缺页中断的可能性。但真正的难点是,如何能找到项目中启动时需要调用的所有方法和函数。