启动优化

2021-10-19  本文已影响0人  浅墨入画

启动性能检测Main函数

启动的过程一般是指从用户点击app图标开始到AppDelegate 的didFinishLaunching方法执行完成为止,其中启动也分为冷启动热启动

下面说的启动优化是指冷启动情况下的,这种情况下应用的启动时间一般分为Main函数执行之前之后

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

image.png image.png
  1. dylib loading time:动态库的载入耗时
    动态库的载入肯定会存在耗时,并且动态库会存在依赖关系。系统动态库存在于共享缓存,但自定义动态库没有这个待遇,所以苹果官方建议不要超过6个自定义动态库,超过可进行多个动态库合并,以此来优化动态库加载的耗时。
    动态库的合并,需要源码才能进行。所以我们只能合并自己开发的动态库,日常使用的三方SDK可能无法合并。
  2. rebase/binding time:重定位符号和符号绑定的耗时(运行时期绑定,编译时期链接)
    rebase:系统采用ASLR技术,保证地址空间随机化。所以在运行时,需要通过rebase进行重定位符号,使用ASLR+偏移地址
    binding:使用外部符号,编译时无法找到函数地址。所以在运行时,dyld加载共享缓存,加载链接动态库之后,进行binding操作,重新绑定外部符号
  3. ObjC setup time:注册OC类的耗时
    注册OC类的过程,读取二进制的data段找到OC的相关信息,然后注册OC类。应用启动时,系统会生成分类的两张表,OC类和分类的注册,会插入到这两张表中,所以会造成一定的时间消耗;
    这部分时间很难优化,除非减少项目中类和分类的定义
    减少类和所属分类load方法的使用,让类以懒加载的方式加载。
  4. initializer time:执行load以及C++构造函数的耗时
    尽可能使用initialize方法代替load方法,或者把一些耗时的操作放入子线程
  5. slowest intializers:列举出几个比较耗时的动态库

这一阶段主要是防止资源浪费(比如OC定义的非常多,自定义动态库非常多),优化建议

虚拟地址的概念

早期的程序比较小,在运行时会将整个程序全部加载到内存中。但随着软件的发展,程序越来越大,导致内存越来越紧张。这就是早期系统中,为什么经常出现内存不足的提示。

早期的数据访问是直接通过物理地址访问的,这种方式有以下两个问题

虚拟内存

针对上面两个问题,我们在进程和物理内存之间增加一个中间层,这个中间层就是所谓的虚拟内存,主要用于解决当多个进程同时存在时,对物理内存的管理。提高了CPU的利用率,使多个进程可以同时、按需加载。所以虚拟内存其本质就是一张虚拟地址物理地址对应关系的映射表

使用虚拟内存的优势:

进程通信由系统提供API使用kernel发送信号。但不能直接跨进程访问,保证数据的安全

虚拟内存与物理内存的关系

虚拟内存:内存分页

页表中记录了内存页的状态、虚拟内存和物理内存的对应关系。其中状态分为:未分配(Unallocated)、未缓存(Uncached)和已缓存(Cached)

缺页异常
页面置换

物理内存的空间是有限的,当内存中没有空间时,操作系统会从选择合适的物理内存页驱逐回磁盘,为新的内存页让出位置,选择待驱逐页的过程在操作系统中叫做页面置换。也就是覆盖掉不那么活跃的物理内存

例如,同一台设备上,依次打开微信、微博、淘宝、京东、抖音,此时再回到微信,又会看到微信的启动界面。因为系统在内存紧张的时候,会按照活跃度将最不活跃的内存进行覆盖

ASLR技术

程序的代码在不修改的情况下,每次加载到虚拟内存中的地址都是一样的,这种方式并不安全。为了解决地址固定的问题,出现了ASLR技术

ASLR的概念:(Address Space Layout Randomization ) 地址空间配置随机加载,是一种针对缓冲区溢出的安全保护技术,通过对共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。

其目的是通过利用随机方式配置数据地址空间,使某些敏感数据(例如APP登录注册、支付相关代码)配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击。

由于ASLR的存在,导致可执行文件动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要在编译时来修复镜像中的资源指针,来指向正确的地址。即正确的内存地址 = ASLR地址 + 偏移值

iOS/MacOS操作系统实现了ASLR

PageFault调试&启动优化

缺页中断消耗

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

PageFault调试
image.png image.png

缺页中断564次,耗时196.95ms。一次缺页中断耗时 大概 0.35ms,冷启动时间 231.17ms

启动优化
image.png image.png
# Symbols:
# Address   Size        File  Name
0x100001E60 0x00000040  [  5] -[SceneDelegate sceneWillResignActive:]
0x100001EA0 0x00000040  [  5] -[SceneDelegate sceneWillEnterForeground:]
0x100001EE0 0x00000040  [  5] -[SceneDelegate sceneDidEnterBackground:]
0x100001F20 0x00000020  [  5] -[SceneDelegate window]
0x100001F40 0x0000008E  [  4] _main
0x100001FD0 0x00000030  [  2] +[ViewController load]
0x100002000 0x00000030  [  3] +[AppDelegate load]
0x100002030 0x00000039  [  2] -[ViewController viewDidLoad]
0x100002070 0x00000080  [  3] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x1000020F0 0x00000120  [  3] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x100002210 0x00000070  [  3] -[AppDelegate application:didDiscardSceneSessions:]
0x100002280 0x000000B0  [  5] -[SceneDelegate scene:willConnectToSession:options:]
0x100002330 0x00000040  [  5] -[SceneDelegate sceneDidDisconnect:]
0x100002370 0x00000040  [  5] -[SceneDelegate sceneDidBecomeActive:]
0x1000023B0 0x00000040  [  5] -[SceneDelegate setWindow:]
0x1000023F0 0x00000033  [  5] -[SceneDelegate .cxx_destruct]
0x100002424 0x00000006  [  6] _NSLog
......
image.png
#import "ViewController.h"
@interface ViewController ()

@end

@implementation ViewController

+(void)load {
    NSLog(@"123");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}
@end

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

按照默认配置,在应用启动时会加载到大量与启动时无关的代码,导致Page Fault的次数增长影响启动时间。如果可以将启动时需要的方法/函数排列在最前面,就能大大降低缺页中断的可能性,从而提升应用的启动速度,这就是二进制重排的核心原理。
以下图为例,方法 1方法 3是启动的时候调用的,为了执行对应的代码,就需要两次Page Fault。假如我们把方法 13排列到一起,那么只需要一次Page Fault,从而提升启动速度。

image.png

二进制重排体验

二进制重排的方案最开始是由抖音的这篇文章抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%火起来的。

链接器ld有个参数-order_file支持按照符号的方式排列二进制。所以需要在工程中创建.order文件,按固定格式将启动时需要的方法/函数顺序排列,然后在Xcode中使用.order文件即可。通过LinkMap文件中的顺序,查看最终的排序是否符合预期。

截屏2021-10-18 下午11.58.50.png
_main
+[ViewController load] 
+[AppDelegate load] 
image.png
# Symbols:
# Address   Size        File  Name
0x100001E60 0x0000008E  [  4] _main
0x100001EF0 0x00000030  [  2] +[ViewController load]
0x100001F20 0x00000030  [  3] +[AppDelegate load]
0x100001F50 0x00000039  [  2] -[ViewController viewDidLoad]
0x100001F90 0x00000080  [  3] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100002010 0x00000120  [  3] -[AppDelegate application:configurationForConnectingSceneSession:options:]
......

最前面三个方法/函数,按照.order文件中的顺序排列

由此可见如果我们将项目中启动时需要调用的所有方法/函数都找到,把它们全部写入到.order文件中,就能大大降低缺页中断的可能性。

二进制重排方案小结:

这里的难点是,如何找到启动时项目调用了哪些方法?

上一篇 下一篇

猜你喜欢

热点阅读