iOS启动流程
1、点击APP图标后,内核创建APP进程
2、将APP的Mach-O可执行文件mmap进虚拟内存,加载dyld程序,接下来调用_dyld_start函数开始程序的初始化
3、重启手机/更新APP会先创建启动闭包,然后根据启动闭包进行相关的初始化
4、将动态库mmap进虚拟内存,动态库数量太多则这里耗时会增加
5、对动态库和APP的Mach-O可执行文件做bind&rebase,主要耗时在 Page In,影响 Page In 数量的是 objc 的元数据
6、初始化 objc 的 runtime,如果有了闭包,由于闭包已经初始化了大部分,这里只会注册 sel 和装载 category
7、+load 和静态初始化被调用,除了方法本身耗时,这里还会引起大量 Page In
8、初始化 UIApplication,启动 Main Runloop
9、执行 will/didFinishLaunch,这里主要是业务代码耗时
10、Layout,viewDidLoad 和 Layoutsubviews 会在这里调用,Autolayout 太多会影响这部分时间
11、Display,drawRect 会调用
12、Prepare,图片解码发生在这一步
13、Commit,首帧渲染数据打包发给 RenderServer,启动结束。
启动速度优化思路:
1、控制APP的可执行文件大小
2、控制动态库数量
3、控制Page In 次数
4、控制首帧渲染前业务逻辑相关耗时
5、控制首帧视图渲染耗时,即上面流程中的步骤10-12
优化启动速度
1、无用资源的优化
1、无用的类
2、无用的方法
3、无用的图片资源
清理方式参见:iOS之安装包优化以及瘦身_风雨「83」的博客-CSDN博客
2、二进制重排
二进制重排为什么会加快启动速度?
当APP进程访问一页虚拟内存page,而对应的物理内存不存在时,先触发缺页中断(Page Fault) 阻塞当前进程,然后加载数据到对应物理内存(Release版本还要对加载的数据进行签名),所以缺页中断还是比较耗时的。假设APP启动时调用100个函数,这100个函数如果分布在100个不同的内存页,那会产生100次缺页中断。如通过二进制重排将这100函数分布到50个或者更少的内存页中,缺页中断的次数减半,启动速度就提升了 。
获取启动阶段Page Fault 的次数
打开Instruments,选择System Trace 工具
重启手机(热启动情况下系统已经做了加载缓存,产生缺页中断大幅减少,所以最好重启手机),然后点击启动,待首屏出现后停止,如下图:?
解决方案
获取启动阶段调用的函数符号然后编写order_file编译顺序文件然后在Build Settings -> Order File 中配置一个后缀为order的文件路径是实现二进制重排的核心思路。目前业界获取启动阶段调用的函数符号主要有三种:
1、抖音通过静态扫描和运行时 Trace 等方法确定 order_file。该方案实现难度大(需要汇编、反汇编等知识),且只能覆盖部分符号(无法覆盖纯Swift、initialize、部分block 和 C++ 通过寄存器的间接函数调用)
2、手淘通过修改 .o 目标文件实现静态插桩,需要对目标代码较为熟悉,通用性不高
3、clang编译器静态插桩,目前业界已有成熟的库和方案
静态插桩: 在 build settings->"Other C Flags"中添加"-fsanitize-coverage=func,trace-pc-guard"。如过项目中有 Swift 代码,还需要在 "Other Swift Flags" 中加入"-sanitize-coverage=fun"和"-sanitize=undefined",如下:
设置完参数-fsanitize-coverage=func,trace-pc-guard 再次运行会报错。报错原因是未到找两个方法,这两个回调方法设置完参数就需要手动去添加。
首先需要引入头文件然后实现缺失的两个方法// 参考文档:SanitizerCoverage — Clang 15.0.0git documentation
#include <stdio.h>
#include <stdint.h>
#include <sanitizer/coverage_interface.h>
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
// if(*guard)
// __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
// __sanitizer_cov_trace_pc_guard(guard);
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
// If you set *guard to 0 this code will not be called again for this edge.
// Now you can get the PC and do whatever you want:
// store it somewhere or symbolize it and print right away.
// The values of `*guard` are as you set them in
// __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
// and use them to dereference an array or a bit vector.
void *PC = __builtin_return_address(0);
char PcDescr[1024];
// This function is a part of the sanitizer run-time.
// To use it, link with AddressSanitizer or other sanitizer.
__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
?再次编译运行,继续报错 未定义的符号 __sanitizer_symbolize_pc
?
?删除以后再次运行编译通过。再次运行。
打印函数数量
打印应用启动所需要的符号表
获取启动需要的所有符号并排重
# pragma mark clang插桩
//https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs-with-guards
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
// printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N;
NSLog(@"函数个数%llu",N);
}
// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
// if(*guard)
// __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
// __sanitizer_cov_trace_pc_guard(guard);
//全局容器存放符号,原子队列,线程安全,先进后出
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
typedef struct{
void *pc;
void *next;
}SYNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// NSLog(@"%s",__func__);
// if (!*guard) return; // Duplicate the guard check.
//*PC指的上一个函数的地址
void *PC = __builtin_return_address(0);
// Dl_info info;
// dladdr(PC, &info);
// NSLog(@"-----%s",info.dli_fname);
// NSLog(@"dli_sname-----%s",info.dli_sname);
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//添加数据函数符号
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
// char PcDescr[1024];
// printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
+(void)getAllStartSymbolList{
NSMutableArray *symbolNames = [NSMutableArray array];
while(YES){
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if(node == NULL){
break;
}
Dl_info info;
dladdr(node->pc, &info);
NSString *name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString *symbolName = isObjc ? name : [@"_"stringByAppendingString:name];
[symbolNames addObject:symbolName];
// if ([name hasPrefix:@"+["] || [name hasPrefix:@"-["]) {
// [symbolNames addObject:name];
// continue;
// }
// [symbolNames addObject:[@"_"stringByAppendingFormat:name]];
// NSLog(@"dli_sname-----%s",info.dli_sname);
}
//对数据进行取反操作
// symbolNames = (NSMutableArray *)[[symbolNames reverseObjectEnumerator] allObjects];
//正向遍历
// NSEnumerator *en = [symbolNames objectEnumerator];
NSEnumerator *en = [symbolNames reverseObjectEnumerator];
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
while (name =[en nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
NSLog(@"%lu",(unsigned long)symbolNames.count);
NSLog(@"funcs %lu",(unsigned long)funcs.count);
//生成order文件
NSString *funcStrs = [funcs componentsJoinedByString:@"\n"];
//写入文件
NSString *filePath = [NSTemporaryDirectory() stringByAppendingString:@"cars.order"];
NSData *file = [funcStrs dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
NSLog(@"%@",NSHomeDirectory());
}
将导出的文件拖到工程主目录下并在build Setting中设置order file文件,
?设置好orderfile之后再次运行程序查看优化前和优化后的时间差
启动时间缩短了0.45秒。
启动link map 顺序查看
build setting 搜索link map 找到Write link map file 设置为YES 然后编译,编译完成后找Product->show build in folder in folder->Intermediates.noindex->Debug-iphoneos找到xx-LinkMap-normal-arm64.txt
?编译顺序根据
设置cars.order编译后link map
设置完成cars.order后再次编译查看link map?
?验证发现link map编译顺序和order文件中的一致,由此可见编译顺序优先安装order编译并启动加载。
|