iOS底层原理探索 —– 启动优化

Main 函数之前的性能检测

应用的启动时间,一般分为Main函数执行之前和之后,执行之前称之为pre-main

系统提供了环境变量,让开发者可以看到pre-main过程中的耗时

查看方式:在Xcode中,选择项目 Product –> Scheme –> Edit Scheme

77674C3C-EC9A-4EAE-8D7E-47BD4E7992FE.png

就能打开设置弹框,然后在 Environment Variables里面添加DYLD_PRINT_STATISTICS环境变量,设置为YES

796CAAF1-79D7-4D67-BDF2-DA8D3B8D2ACE.png

运行项目,就能打印出对应的耗时信息:

F0D35C3A-18A1-41CB-9DF5-3B999A480AFF.png

  • 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技术。

ASLRAddress space layout randomization):是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。

大部分主流的操作系统已经实现了ASLR

  • Linux:在内核版本2.6.12中添加ASLR

  • WindowsWindows Server 2008Windows 7Windows VistaWindows Server 2008 R2,默认情况下启用ASLR,但它仅适用于动态链接库和可执行文件;

  • Mac OS XAppleMac OS X Leopard10.52007年十月发行)中某些库导入了随机地址偏移,但其实现并没有提供ASLR所定义的完整保护能力。而Mac OS X Lion10.7则对所有的应用程序均提供了ASLR支持。Apple宣称为应用程序改善了这项技术的支持,能让3264位的应用程序避开更多此类攻击。从OS X Mountain Lion10.8开始,核心及核心扩充(kext)与zones在系统启动时也会随机配置;

  • iOSiPhoneiPod touchiPad):AppleiOS4.3内导入了ASLR

  • AndroidAndroid 4.0提供地址空间配置随机加载(ASLR),以帮助保护系统和第三方应用程序免受由于内存管理问题的攻击,在Android 4.1中加入地址无关代码(position-independent code)的支持。

二进制重排

缺页中断的消耗

当系统访问虚拟内存时,发现数据还未加载到物理内存中,会触发缺页中断(Page Fault),造成进程阻塞。此时系统会先将数据加载到物理内存中,进程才能继续运行。虽然每一页数据加载到内存的速度很快,毫秒级别,但在应用冷启动时,可能会出现大量的缺页中断,对启动速度带来一定的时间消耗。

使用测试项目,查看应用在启动过程中,Page Fault所带来的消耗。

Xcode菜单中,选择Product –> Profile

921BDF7E-AE83-4AA4-87CB-6ADE9A1C2055.png

打开Instruments

C253BFE4-7D7F-4E21-B2F9-1577742FE0A7.png

运行测试项目,当第一个界面出来后即可停止,搜索main thread

  • 一个小测试项目,启动时缺页中断564次,耗时200毫秒。如果是微信、抖音等大型项目,不进行优化可达到6000次以上,造成不小的时间消耗。

二进制重排的原理

搭建测试项目,查看代码顺序。

打开项目,在Build Settings –> Write Link Map File,设置为YES

D49157C7-B79B-4E75-ADF2-7E9437E79002.png

编译项目,来到工程的Build目录下,找到LinkMap文件:

image.png

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 
...
复制代码

文件编译顺序是XcodeBuild Phases –> Compile Sources的文件排列顺序:

219926E3-05CC-41F5-A871-9CAC4E6CF299.png

文件中方法/函数的符号顺序,就是代码的书写顺序:

A535F441-EF29-45B4-B2D1-4838C71ECA68.png

  • ViewController.m为例,load方法在viewDidLoad方法之前,和LinkMap文件中的顺序一致。

所以,按照默认配置,在应用启动时,会加载到大量与启动时无关的代码,导致Page Fault的次数增长,影响启动时间。如果可以将启动时需要的方法/函数排列在最前面,就能大大降低缺页中断的可能性,从而提升应用的启动速度,这就是二进制重排的核心原理。

二进制重排的配置

二进制重排的配置非常简单,只需要在工程中创建.order文件,按固定格式,将启动时需要的方法/函数顺序排列,然后在Xcode中使用.order文件即可。通过LinkMap文件中的顺序,查看最终的排序是否符合预期。

在工程根目录创建.order文件

image.png

打开hk.order文件,写入启动时需要的方法/函数:

+[ViewController load] 
+[AppDelegate load] 
_main
复制代码

Xcode使用.order文件,在Build Setting –> Order File中配置:

49C806F0-5552-47D4-AF2D-8762F5541F08.png

编译项目,打开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文件中,就能大大降低缺页中断的可能性。但真正的难点是,如何能找到项目中启动时需要调用的所有方法和函数。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享