一、启动优化
冷启动:杀死app后的第一次启动
热启动:app还在后台运行,这个时候点开app
启动优化:一般讲的是冷启动
启动阶段:main函数之前、main函数之后
main 阶段:
1、懒加载
2、发挥CPU的价值(多线程进行初始化)
3、启动时避免使用Xib、stroyboard阶段一、main函数之前
打印启动时间
添加 DYLD_PRINT_STATISTICS
dylib loading :加载可执行文件(App 的.o 文件的集合), 加载动态链接库;(优化:建议不要大于6个)
rebase/binding :对动态链接库进行 rebase 指针调整和 bind 符号绑定; 修正内部偏移指针/外部符号绑定 (优化:减少OC类) 优化少
Objc setup :Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等;(优化:减少OC类) 优化少
initializer:包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量 (优化:使用懒加载)
a.减少加载动态库的数量
谈到优化,映射到我们头脑中的***个想法就是:少做事!减少进程启动过程中的动态库数量,就成了当务之急。这里介绍几个方法:
(1)将一些无用的动态库去掉。有些程序员为了自己编程方便,把一些动态库不管是否真的使用全都链接上,这导致进程启动过程中加载了一些无用的动态库,浪费了时间。这些无用的动态库应坚决去掉。
(2)重新组织动态库的结构,力争将进程加载动态库的数量减到最少。对于使用标准C编写的动态库,可以考虑将几个小动态库合并为一个大的动态库,减少进程加载动态库的数量。
对于使用C++编写的动态库,由于涉及全局对象初始化的问题,笔者建议将大的动态库拆分为若干个小的动态库,进程根据自己的需要灵活加载所需要动态库。对于那些经常一同出现的动态库,可以考虑将其进行合并。
关于这点,可以参考2.1.5节,那里有更加详细的论述。
(3)将一些动态库编译成静态库,与进程或其他动态库合并,从而减少加载动态库的数量。其优点是:
减少了加载动态库的数量。
在与其他动态库(或进程)合并之后,动态库内部之间的函数调用不必再进行符号查找、动态链接,从而提高速度。
缺点是:
该动态库如果被多个动态库或进程所依赖的话,那么该动态库将被复制多份合并到新的动态库中,导致整体的文件大小增加,占用更多的Flash。
失去了动态库原有的代码段内存共享,因此可能会导致代码段内存使用上的增加。
如果该动态库被多个守护进程所使用,那么其代码段很多代码已经被加载到物理内存,那么进程在运行该动态库的代码时产生的page fault就少;如果该动态库被编译成静态库与其他动态库合并,那么其代码段被其他多个守护进程运行到的机会就少,在进程启动过程中运行到新的动态库时所产生的page fault就多,从而有可能影响进程的加载速度。
基于此,在考虑将动态库改为静态库时,有以下原则:
对于那些只被很少进程加载的动态库,要将其编译成静态库,从而减少进程启动时加载动态库的数量;同时由于该动态库代码段很少被多个进程共享,所以不会增加内存方面的开销。
对于那些守护使用的动态库,其代码段大多已经被加载到内存,运行时产生的page fault要少,故其为动态库反而有可能要比静态库速度更快。
(4)使用dlopen动态加载动态库。进程所依赖的动态库,并不一定在进程启动时都要用到。不需要的动态库,要在进程启动时加载动态库的清单中去掉,从而加快进程的启动速度。在需要该动态库时,再使用dlopen来动态加载动态库。
dlopen的优点是:可以精确控制动态库的生存周期,一方面可以减少动态库数据段的内存使用,另一方面可以减少进程启动时加载动态库的时间。
其缺点是:程序员编写程序将变得很麻烦。
减少动态库的个数,如果太多就使用合并的方式控制,这样可以节约dylib loading及rebase/binding的时间
清理项目中未用到的类、类别、方法等,这样可以节约Objc setup的时间
对于可以不在+load中处理的逻辑可以放到其他的函数中去处理,比如:+initialize;控制 C++ 全局变量的数量;这样可以节约initializer的时间
阶段二、main函数之后
main 开始 到 第一个界面。
打点,使用BLStopwatch.h和BLStopwatch.m这个类
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[BLStopwatch sharedStopwatch] start];
int a = 0;
for (int i = 0; i < 10000000; i++) {
a++;
}
[[BLStopwatch sharedStopwatch] splitWithDescription:@"didFinishLaunchingWithOptions"];
return YES;
}
- (void)viewDidLoad {
[super viewDidLoad];
//刷新时间:
[[BLStopwatch sharedStopwatch] refreshMedianTime];
int a = 0;
for (int i = 0; i < 10000000; i++) {
a++;
};
[[BLStopwatch sharedStopwatch] splitWithDescription:@"viewDidLoad"];
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
//刷新时间:
[[BLStopwatch sharedStopwatch] refreshMedianTime];
int a = 0;
for (int i = 0; i < 10000000; i++) {
a++;
};
[[BLStopwatch sharedStopwatch] splitWithDescription:@"viewDidAppear"];
[[BLStopwatch sharedStopwatch] stopAndPresentResultsThenReset];
}
二、二进制重排
二进制重排是在main函数之前
物理内存
虚拟内存 : 解决安全问题、解决内存使用率问题
解决安全问题:映射表(页表)(虚拟页表)
解决内存使用率问题:内存分页管理。缺页中断,然后加载到物理内存,加载之前会签名加载的页;如果启动的时候要加载的代码分别在不同的页,那么缺页中断时间就比较长,这时就出现了二进制重排(把启动要加载的代码放在前面几页)。使用内存分页后,就会导致代码的加载都是从0开始的,为了防止黑客,就出现了ASLR。
内存分页技术
MacOS 、linux (4K为一页)
iOS(16K为一页)
PageFault(缺页中断)
进程如果能直接访问物理内存无疑是很不安全的,所以操作系统在物理内存的上又建立了一层虚拟内存。为了提高效率和方便管理,又对虚拟内存和物理内存又进行分页(Page)。当进程访问一个虚拟内存Page而对应的物理内存却不存在时,会触发一次缺页中断(Page Fault),分配物理内存,有需要的话会从磁盘mmap读人数据。
通过App Store渠道分发的App,Page Fault还会进行签名验证,所以一次Page Fault的耗时比想象的要多:
- 2.3 重排
编译器在生成二进制代码的时候,默认按照链接的Object File(.o)顺序写文件,按照Object File内部的函数顺序写函数。
- 静态库文件.a就是一组.o文件的ar包,可以用ar -t查看.a包含的所有.o
简化问题:假设我们只有两个page:page1/page2,其中绿色的method1和method3启动时候需要调用,为了执行对应的代码,系统必须进行两个Page Fault。
但如果我们把method1和method3排布到一起,那么只需要一个Page Fault即可,这就是二进制文件重排的核心原理。
- 2.4 Xcode配置Order
那么我们需要将启动时候调用的函数进行重排,让它们尽可能的分配在同一个页;比如load方法我们就将其找出来,放到一起;LLVM支持我们通过设置order来达到这个效果
- 2.4.1 首先打开Write Link Map File查看
Link Map File中文直译为链接映射文件,它是在Xcode生成可执行文件的同时生成的链接信息文件,用于描述可执行文件的构造部分,包括了代码段和数据段的分布情况
我们可以在Xcode的配置中将Write Link Map File设置为YES来生成Map File