基本概念
进程如果能直接访问物理内存无疑是很不安全的,所以操作系统在物理内存的上又建立了一层虚拟内存。下面是进程的物理内存和虚拟内存之间的映射关系图。
a)存储安全
通过这个映射表才能知道应用的进程所生成的二进制数据都是存储在物理内存的哪个地址里面。而且相应的进程永远只能访问自己通过映射表访问到的物理内存区域,不能够访问其他的区域,这一点就很大程度上保证了存储数据的安全可靠。
b)内存不够用的问题
我们加载进程的时候,进程会碰到物理内存分配不够的问题,实际上通过虚拟内存块映射到物理内存上的时候,并不是一次性把所有的内存都加载到物理内存区。而是加载那些活跃的已经用到的数据在虚拟内存上的所表示的页块到物理内存中去。如下图所示
进程1中的1,3,5区域的数据是活跃的,那么就会把相应的在虚拟表中的内存映射到物理内存中去。同理进程2中的1,5也加载到了物理内存中。这样的话就能大大增加我们设备的内存所能承载的进程数量。因为进程虽多,但是活跃的数据并不是那么多,所以物理内存是够用的。
这里补充一点,就是内存分页管理,内存是一页一页的映射到物理内存中去的。我们查找数据都是通过页的机值+偏移值 来定位内存中的数据的。同时linux和macOs 的页的大小是4k,ios设备上是16k。通过pagesize 指令就能看出一页是多大。
综上,我们通过分页来实现的虚拟内存就能解决内存不够用的问题。同时我们的进程在访问数据的时候只能访问映射表(虚拟页表),不能直接访问物理内存。
c)缺页异常
还是如上图所示,内存中存在进程1的1,3,5页。但是如果我们突然使用某个功能,用到了数据2也就是p2中的数据,那么其实在物理内存中是没有p2的数据的,这个时候操作系统就会产生一个缺页中断(缺页异常)。但是实际我们触发进程的某个功能的时候产生的缺页中断的情况并不多,而且操作系统 通过mmu去处理这个缺页中断时间是非常快的(毫秒级别),感知不到。如果内存已经满了,这个时候进程发出的新的数据产生的缺页中断只能通过页面置换来处理,就是替换掉内存中部活跃的内存页。这里有个简单的例子就是手机开启多个应用后一段时间后,再去打开最先打开的应用比方说微信,那么这个时候微信就会重启,因为在物理内存中的数据已经被页面置换掉了。
由于缺页异常是毫秒级别的,基本可以忽略不计,而且我们进程操作的时候触发的缺页异常很少,所以这一点的缺页异常我们可以不用考虑了。但是有一种情况下的缺页异常会大幅度增加。如下
d)应用启动导致缺页异常同时大量出现
因为我们在启动的时候所有的数据都从不活跃到活跃,启动的瞬间会有大量的数据要加载,很多的方法要调用。就会发生大量的缺页异常,而且是在同一时间内。上面我们说的缺页异常可以忽略不计,但是这一次用户就能感知到了,这就反应在启动时间上。比如微信的启动时间大概是1秒左右,苹果建议的一款app的启动时间大概是400ms。这种启动耗时优化也是我们面对一款大型应用所必须具备的技能。
e) 启动优化—缺页异常次数的优化—查看启动调用类和方法
经过我们上面的分析可以得知,我们针对app的启动优化其实就是缺页异常次数的优化,这也是从技术层面的优化。如果我们能降低缺页异常的次数,那么就能缩短启动时间。与此同时,我们光从启动优化的角度,除了减少缺页异常次数这个方法以外,还有就是可以通过合并动态库来进行启动优化。今天我们主要还是讨论通过减少缺页异常次数来进行优化。
我们可以通过xcode的buildsetting里的link map的是否写入来看到我们项目里所有的类,已经类的方法的编译顺序。如下图所示
我们设置完yes之后,build一下,可以在项目编译生成的.app文件夹里的后缀名未noindex的文件夹里找到xxx.build->Debug-iphoneos->xxx.build->LinkMap-normal-arm64.txt,打开看之后我们能看到我们的类和类的方法调用的顺序如下图
这个文件的加载顺序是在我们项目的编译配置里面的Compile Sources中能看到,如下图所示。
这里如果我们变换了这个顺序,那么我们在上面的linkMap文件中的加载的类的顺序也会随之变化,同时类里面的方法从上而下的顺序也决定了linkMap中加载的方法的顺序,这里大家可以自己试一下,就不做过多赘叙。
f)缺页异常优化的原理
上面我们讲到了启动加载类和方法的查看方法。这里我们开始学习缺页异常优化的原理。如果说我们应用启动的时候,我们启动的数据和方法占用了虚拟页表中的500页,那么我们启动的时候就要进行500次的缺页异常处理。但是这500页里并不全都是我们启动的时候需要调用的方法。那么我们可以想到把启动时需要调用的方法集中到前100页,这样后面的400页就可以不用触发了,就可以把缺页异常的次数减少到100页了。简单来说就是通过改变启动的类和方法的调用顺序来减少缺页异常优化,这就是所谓的二进制重排。
上面我们说到了可以通过改变compile source中的顺序以及类的方法的顺序来改变调用顺序,但是实际我们在进行二进制重排的时候不可能这么去干的,这个时候我们采用了一个order_file文件来进行编译顺序的重排。如下图所示
我们改变了一个类的顺序成这样之后,接下来把这个文件存到我们工程目录下,然后再改变build setting里的order file的文件路径,如下图所示
我们再编译一次之后,看上面的那个LinkMap文件中的方法编译顺序,如下图
这个顺序就变成了我们上面order file文件中的顺序了。
tips:这里有一个调试第三方app的方法,有待研究。补个截图
通过.sh脚本 来运行微信.ipa
g)查看项目的 缺页异常次数
通过 xcode->开发者工具->instruments->system trace,如下图 File Backed Page In
这个就是微信的缺页异常的次数。
tips:fishhook 可以hook系统函数,当我们去hook objc_msgSend就能拿到所有的方法调用。但是也不是百分百OK,像block和C函数这些还是拿不到的。Clang是我们编译器LLVM的前端,
h) 最关键的一步—重排二进制文件
这里面C++静态初始化和Local符号还有block的流程我就省略了,难度较大。这里保留了参考资料——抖音团队的优化方案,文章链接https://mp.weixin.qq.com/s/Drmmx5JtjG3UtTFksL6Q8Q
重点说说OC的操作流程和方案,通过trace动态库 注入 做一次app的运行到启动结束。这一步做了从运行到启动结束的标记。再通过hook objc_msgsend 拿到启动调用的所有oc的方法(这里只有地址貌似)根据地址再去反查LinkMap中的方法,把所有的方法按照顺序写入order_file 文件,之后在app里重新设置orderfile文件,就可以完成OC的二进制重排了。
后续有问题的可以一起交流!