ios关联启动_iOS启动优化(Clang插桩)
⼀、查看APP启动耗时
main函数之前的处理为pre-mian阶段,这篇⽂章主要分析这个阶段。
添加DYLD_PRINT_STATISTICS参数打印出pre-mian阶段的耗时情况:
各时段处理耗时分析:
Total pre-main time: 总耗时
dylib loading time: 动态库载⼊耗时
rebase/binding time: rebase表⽰地址偏移修正(ASLR),binding表⽰符号绑定
ObjC setup time: OC类注册耗时
initializer time: 执⾏load和构造函数的耗时
slowest intializers :
libSystem.B.dylib : 系统的
libMainThreadChecker.dylib :
XXXXX : 项⽬主程序耗时
pre-main优化⽅向:
官⽅建议⾮系统动态库的加载个数不超过6个,多于6个就要考虑动态库的合并;
减少OC类,减少C++虚函数
减少load⽅法和构造函数
main⽅法之后优化⽅向:
延迟初始化、懒加载
删除不使⽤类、⽅法、图⽚资源
⼆、虚拟内存和物理内存
1、虚拟内存和物理内存的区别
当我们向系统申请内存时,系统并不会给你返回物理内存的地址,⽽是给你⼀个虚拟内存地址。CPU读取数据时也是通过内存管理单元MMU将虚拟地址映射到物理内存地址。每个进程都拥有相同⼤⼩的虚拟地址空间,对于32位的进程,可以拥有4GB的虚拟内存,64位进程则更多,可达18EB。只有我们开始使⽤申请到的虚拟内存时,系统才会将虚拟地址映射到物理地址上,从⽽让程序使⽤真实的物理内存。
2、内存分页
系统会对虚拟内存和物理内存进⾏分页,虚拟内存到物理内存的映射都是以页为最⼩粒度的。在OSX和早期的iOS系统中,物理和虚拟内存都按照4KB的⼤⼩进⾏分页。iOS近期的系统中,基于A7和A8处理器的系统,物理内存按照4KB分页,虚拟内存按照16KB分页。基于A9处理器的系统,物理和虚拟内存都是以16KB进⾏分页。(终端输⼊$PAGESIZE可以查看到macOS的分页⼤⼩)。
系统将内存页分为三种状态。
活跃内存页(active pages)- 这种内存页已经被映射到物理内存中,⽽且近期被访问过,处于活跃状态。
⾮活跃内存页(inactive pages)- 这种内存页已经被映射到物理内存中,但是近期没有被访问过。
当可⽤的内存页降低到⼀定的阀值时,系统就会采取低内存应对措施,在OSX中,系统会将⾮活跃内存页交换到硬盘上,⽽在iOS中,则会触发Memory Warning,如果你的App没有处理低内存警告并且还在后台占⽤太多内存,则有可能被杀掉。
3、如何解决内存浪费的?
应⽤程序加载到内存中时,并不会全部加载到物理内存中,属于懒加载,⽤哪⼀部分就加载那⼀部分。当访问进程的内存地址时,⾸先看页表,查看所要访问的对应页表是否已经加载到内存中。如果这⼀页没有在物理内存中时,操作系统会阻塞当前进程,发出⼀个缺页异常/缺页中断(pagefault),让后将磁盘中对应页的数据加载到内存中,完成虚拟内存和物理内存的映射。
当前进程的页表数据加载到物理内存中时,不⼀定是连续的,也有可能会覆盖其他进程的不活跃页,这样的按需分配,极⼤提⾼内存的使⽤效率。
4、虚拟内存的安全问题
虚拟内存通过页表映射到物理内存上,因此直接访问物理地址并不能实际正确的拿到进程的数据,但是进程的虚拟内存地址相对于⾃⼰来说也是绝对的,不管程序运⾏多少次,如果访问同⼀个函数,它
在虚拟内存中的地址都是⼀样的这样也存在安全问题(⽐如直接静态注⼊)。
这样也出现了新的技术--ASLR(Address Space Layout Randomization)。
每次虚拟内存在加载之前,都加⼀个随机偏移值。
三、⼆进制重排原理
1、什么是⼆进制重排
缺页中断/缺页异常:内存分页管理,每⼀页加载的时候都会发⽣。
在iOS中,在加载缺页内存的时候,不仅发⽣缺页阻塞从磁盘中加载数据,还要对加载的这页做签名验证。
在App使⽤中不会发⽣⼤量的pagefault,我们⼀般感受不到这个过程。但是在启动时,程序有⼤量的代码需要加载、执⾏,那么这个缺页中断有可能就很明显了。
如何优化?
假如我的App只有10页数据,但是启动的时候需要加载的代码分散放在1、3、5页。因为代码在Mach-
o⽂件中的位置是根据⽂件加载⽣成的顺序来决定。那么这时候App启动需要运⾏的代码放在3个虚拟内存页中就会出现3次pagefault。
如果我们将需要启动⽤的代码全部放在第1页中,那么App启动时便只会触发⼀次pagefault,App启动加载的数据也会变少,这样极⼤减少进程的阻塞。这就是⼆进制重排的原理。
2、查看pagefault
Xcode提供相关的调试⼯具,打开Instruments-System Trace,选中⼿机中的App,点击System Trace左上⾓开始记录后会⾃动打开⼿机中的App,进⼊⾸屏后点击System Trace左上⾓停⽌。查看Main Thread中虚拟内存的File Backed Page In项⽬,它代表着启动时产⽣的pagefault次数。
查看pagefault次数时受App冷启动热启动影响很⼤,可以先开启⼏个其他App然后等⼀段时间再点击System Trace左上⾓开启记录。
⼆进制重排的优化是发⽣在编译链接阶段,对即将⽣成的⼆进制可执⾏⽂件进⾏重排。
Xcode使⽤的连接器叫ld它可以指向⼀个order_file⽂件,在这个⽂件中指定排列符号,那么Xcode在编译时会按照指定的排列编译出可执⾏的⽂件,苹果objc源码项⽬中的der⽂件就是实现⼆进制重排功能的。
四、实现⼆进制重排
1、查看⽅法排列顺序
在项⽬的build settings中搜索link map开启这个⽂件的输出
重新编译后就可以在⼯程的build⽬录⾥⾯到⼀份link map⽂件
Xcode -> DerivedData-> 项⽬名-> Build-> index-> 项⽬名.build-> Debug-iphoneos-> 项⽬名.build-> 项⽬名-
这个⽂件⾥⾯就记录⼀些链接.o的⽂件、Mach-o⽂件⾥的⼀些信息、符号信息symbols等等…
注意,这个symbols就是关注的要点:默认情况下它是按照Build Phases-Compile Sources中编译⽂件从上⾄下排序以及类中⽅法从上⾄下排序。
2、通过order⽂件重新排列加载顺序:
在⼯程配置中,添加⼀个指定符号顺序的. order⽂件后,让编译器按照指定的顺序重新排列⼆进制⽂件,把最需要加载的代码段放在内存页靠前的位置。
这⾥只是演⽰了让viewcontroller中的⼏个⾃定义⽅法优先靠排列在内存分页中,实际中⼀个app启动时的pagefault可能多达⼏千次,那么需要重排的函数远不⽌这⼀点。
五、Clang插桩
1、引⼊Clang插桩
由于项⽬中存在⼤量的函数⽅法调⽤,此外还有Block、Swift、C函数,因此仅仅HOOK msgSend⽅法不可⾏。因为Clang会读取所有代码,分析AST中所有节点,所有通过Clang插桩可以实现100%的符号覆盖。
2、使⽤Tracing PCs
根据官⽅⽂档添加-fsanitize-coverage=trace-pc-guard标记
ViewController.m中添加两个官⽅⽂档中的⽅法实现:
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.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
char PcDescr[1024];
xcode怎么打开
//__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
-[ViewController viewDidLoad]前添加断点,运⾏项⽬,到断点后打开汇编断点(菜单栏Debug->Debug Workflow->Always Show
结合汇编中插⼊的__sanitizer_cov_trace_pc_guard代码和控制台打印的信息分析可知:添加-fsanitize-coverage=trace-pc-guard标记后Clang会在中间代码IR中的每个⽅法、Block等调⽤边缘插⼊__sanitizer_cov_trace_pc_guard⽅法的调⽤。
所以Clang插桩插⼊的就是__sanitizer_cov_trace_pc_guard⽅法调⽤。
3、修改__sanitizer_cov_trace_pc_guard⽅法,获取函数的调⽤⽅法名
#import
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//排除load⽅法
if (!*guard) return;
//当前函数返回到上⼀个⽅法继续执⾏的地址
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("fname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n\n\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
char PcDescr[1024];
//__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
}
点击屏幕输出:
fname:/private/var/containers/Bundle//Test_TracingPCs.app/Test_TracingPCs
fbase:0x10236c000
sname:-[ViewController touchesBegan:withEvent:]
saddr:0x102371ad4
4、将获取到的符号. order⽂件
#import //⽤于定义原⼦队列
//定义原⼦队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
void *pc;
void *next;
} SYNode;