Peter's Den

悲观者只见到机会后面的问题,乐观者却看见问题后面的机会

Hello,在下2012年涉足Apple Developer,至今在iOS/OSX领域混迹多年,本职工作以iOS为主


精通Objective-c/Swift,对Python/Java/.Net/JavaScript也略懂一二,会与大家在这里记录分享

iOS启动优化之二进制重排

启动优化

关于启动优化,在客户端开发中是一个老生常谈的话题,图片压缩/删除代码等方式都已经老掉牙了,一般的面试中也回答这些方案,在众多面试者中也无亮点。

下面的文章主要来讲讲,利用二进制重排的方案,来做优化启动时间。

概念

在讲重排之前,我们需要了解几个概念。

虚拟内存和物理内存

  • 物理内存:内存条(硬件)上的地址就叫物理地址,以前的计算机都是通过CPU访问物理内存拿到数据的,这样存在安全问题(金手指?),而且随着软件发展速度快于硬件速度,物理内存也越发不够……
  • 虚拟内存:在app运行时,CPU首先会访问虚拟内存地址,MMU内存管理单元配合操作系统负责地址翻译(虚实地址互转),这样CPU就能访问物理内存地址读取数据了。虚拟内存实际就是一张表,保存着和物理内存的映射表。
    • 虚拟地址都是从0开始,黑客们可以通过偏移拿到其他地址的数据,ASLR(Address Space Layout Randomization)技术就是为了解决虚拟地址固定不变导致安全问题而出现的。
  • 内存分页管理:数据会以页的方式来存储,方便管理(Linux和MacOS每页数据是4k,iOS是16k),每个app会被分为若干页,运行的时候会一页一页按需加载。当数据需要被使用,而且没有被加载进页表(虚拟内存里)的时候,这个时候会触发“中断”(缺页异常PageFault),系统会让将数据载入到物理内存,这个时候再去取。

Page Fault

什么是Page Fault?

当进程访问它的虚拟地址空间中的PAGE时,如果这个PAGE目前还不在物理内存中,此时CPU是不能干活的,Linux会产生一个hard page fault中断。系统需要从慢速设备(如磁盘)将对应的数据PAGE读入物理内存,并建立物理内存地址与虚拟地址空间PAGE的映射关系。然后进程才能访问这部分虚拟地址空间的内存。

Page Fault又分为以下几种:

  • major page fault
    • 也称为 hard page fault, 指需要访问的内存不在虚拟地址空间,也不在物理内存中,需要从慢速设备载入。从swap 回到物理内存也是 hard page fault。
  • minor page fault
    • 也称为 soft page fault, 指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可。
  • invalid(segment fault)
    • 也称为 segment fault,指进程需要访问的内存地址不在它的虚拟地址空间范围内,属于越界访问,内核会报 segment fault错误

开始表演~

那么,我们写的一个app,如何知道启动的时候总共有多少次page fault呢?

请打开Instrument,打开system trace来启动app,我们以微信为例:

它启动总共有3040次page fault,耗时365ms,如果我们可以减少这个page fault次数,是不是就能减少启动时间呢?

答案是肯定的。

先来说说为什么会出现那么多次的page fault呢?

前面我们有提到,iOS中,每个page的size是16k,一页放不下时就需要放到第二页,类似于我们列表页的分页。我们代码中每个函数在符号表中都是有加载顺序的并存放在page中,在xcode -> build settings -> link map设置,就可以看到linkmap导出的txt文件,如图:

下面画一个简单的图来表示下page,==蓝色==代表普通函数,==红色==代表启动时需要执行的函数:

那么如果,我们把函数的顺序修改下呢?

设想一次,像微信一样体量的app,如果重排后,是不是可以大大的优化启动时间。

那么问题来了,怎么重排呢?

前面提到了link map,我们先用demo试试。

我在控制器里只写了这3个函数

然后导出下link map看下:

可以看到,这里的顺序是我们代码的书写顺序,是的,没错。

这3个函数,只有load是启动前会执行,test1与test3不需要执行,所以应该把他重排的后面去,我们改下书写顺序就可以了。

那么问题又来了,有没有发现,我们的test1与test2都是在main函数前面,证明这样手动的换顺序还不够,再说我们不止一个控制器,也不止一个模块,所以这种方案是不行的。

我们在看objc源码的时候,可以发现目录中有一个libobjc.order文件,其实它也做了二进制重排。

我们自己写创建一个order排序文件:

然后,我们build下,再看看link map.txt

没错,成功了,test1与test2就在启动后了

很多同学又想问了,一个app这么多代码,难道手动整理出哪些是启动前需要的?

当然不是哦,我们可以通过hook objc_msgSend,这样就可以知道哪些函数是在启动前执行了

不过这种hook是hook不了block等的,此时就需要利用clang插桩/汇编插桩,本文重点不hook与插桩,所以不在这里过多讲述。

以上,就是二进制重排。

最近的文章

mach-o动态注入原理

mach-o动态注入原理 本文主要讲解开源工具yololib的实现原理,超级简单,源码总共也就200多行,并无多大技术含量,主要是为了记录备忘命令// 命令基本用法yololib <mach-o> <dylib file>入口代码 可以看出,这里是记录了2个路径,然后调用了inject_file函数函数:inject_file 步骤: fopen打开mach-o fread读取mach-o信息 根据读取到的bu...…

iOS继续阅读
更早的文章

iOS逆向开发

iOS逆向开发 现在越狱的同学越来越少了,因为苹果经过这么多年借鉴越狱开发者的功能使得iOS本身已经很方便,以至于用户没有太大的必要去越狱了。 但是作为一名iOS开发者,逆向开发还有必要的,不管是学习他人的App还是自己想做一些功能,例如微信抢红包啥的。 下面所提到的所有逆向都支持安装在非越狱的iPhone上准备工具 一台已越狱的iPhone(用来砸壳):本文用的是iOS13.2.2 yololib:https://github.com/gaoshilei/yololib fr...…

iOS继续阅读