二进制重排与Clang插桩
背景
- 随着app的迭代,日常业务变多,项目复杂度变高,引起app的启动越来越缓慢
那怎么优化app的启动呢?
一般app启动分main函数之前, 和main函数之后,那么App的启动优化也可在main之前(pre-main)和main之后。
最早执行代码的地方为+load方法。那么有什么方式可以控制这些顺序吗? - 大民哥带你认识iOS中二进制重排与Clang插桩
优化方案
Dyld反馈资源浪费:毫秒级
业务逻辑
Main函数之前 pre-main
Main函数之后
准备
objc
源码clang
脚本
认识
插件安装
启动优化
- iOS检测:dyld会把app启动耗时反馈给我们
1,dyld重签名,然后设置PRINT_STSTISTICS
运行:
Dyld反馈等都是资源的浪费,基本上也是毫秒级的浪费!
我们需要看的是 - dylib loading:苹果建议的动态库载入不超过6个
- rebase:虚拟内存,虚拟内存载入到物理内存发生缺页异常时,根据ASLR重写矫正地址,随机起始值 不同偏移量,ASLR+offsert
- binding:绑定,以懒加载绑定
- ObjC:OC类的注册,减少类的定义,分类的定义(实际项目中去除废弃不用的类)
- initalizer:执行+load、构造函数的耗时
下面的几个系统库已做了高度优化,不做研究。
操作系统
演进史
- 物理内存的时代:内存不足,不安全,
- 物理内存切片时代:使用懒加载,但内存不连续了,麻烦和不安全
- 虚拟内存时代:使用虚拟内存 存放于映射表中,CPU的MMU(翻译地址)翻译成物理地址,然后到物理内存中找到物理内存Page(页表),PageiOS中16k,Mac中4k,
解决内存不够用的问题
,进程与进程之间的安全隔离保证了内存的安全,每次访问只访问虚拟内存对应的物理地址的那一页数据。 - 按需加载,分页加载
PAGESIZE
- 当虚拟页表中的内存在物理内存中没有的时候会进入缺页中断Pagefault
- 这个时候新的虚拟内存要加入物理内存中,执行LRU算法,在物理内存中覆盖不活跃的进程
- 虚拟页表中最后会有些空的内存空间,访问时候为NULL
- 在64位系统里面,虚拟内存8G可以访问小于4G物理内存,因为有4个G的空间不让使用,因为要隔离兼容前面32位系统的4个G,
段:MachO文件格式,可变
页:内存里面的单位,固定
CPU数据吞吐量(数据总线),32位4字节,64位8字节
进程通信
- 进程间通信,是通过kernel发信号,虚拟内存的共享缓存空间 访问物理内存的共享访问空间
二进制重排
如果同时有大量缺页异常,那就影响启动时间了(冷启动)
- PageFault(缺页异常/中断)毫秒级
引入脚本appSign.sh,注入WeChat二进制文件.app
第一次启动(冷启动),我们看到缺页异常6000+次,启动耗时1秒多,当再启动时(热启动),缺页异常1000+次,启动耗时3.多毫秒。
那么我们怎么优化冷启动时间呢?(主要跟PageFault浪费有关)
- 排列二进制时,把启动时需要调用的方法全部向前排,最大化优化启动时Pagefault次数
.Order文件
order文件里的顺序就是给编译器看的编译顺序文件。
- 第一步:工程目录文件建立.order文件
- 然后,在.order文件中写入编译顺序
- Xcode->Build Settings -> order File写入(./文件名称.order)
小节问题
二进制重排是没问题了,那么问题来了,App启动时方法的调用顺序又是什么呢?
还有OC与Swift混编的调用顺序呢?
方法,c函数,Block这些顺序怎么看呢?
- hook objc_msgSend();能解决OC方法,但其他的又都hook不到,那么Clang就来了
Clang插桩
Clang会读程序中所有的代码
Clang插桩配置
Clang找到Trance PC
添加-fsanitize-coverage=trace-pc-guard
到工程Other C Flag:
Clang插桩原理(获取及通过原子队列保存符号)
然后在ViewController里面声明头文件和实现:
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
实现方法:
//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
void *pc;
void *next;
} SYNode;
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);
//指针偏移4字节间隔,最后一个数据为stop - 4,防止内存溢出
for (uint32_t *x = start; x < stop; x++)
*x = ++N;
}
//启动时调用了哪些方法函数以及顺序,在这里都会被hook,一切的回调函数,无论线程
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return;
//当前函数返回到上一个调用的地址!!
void *PC = __builtin_return_address(0);
//创建结构体! 及大小
SYNode * node = malloc(sizeof(SYNode));
//结构体指针赋值
*node = (SYNode){PC,NULL};
//结构体入栈,加入结构(链表结构)!- 列表头,存入node,标记SYNode类型,next:把下一个内存地址返回给node的next
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
//写入.order文件
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//定义数组
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (YES) {//一次循环!也会被HOOK一次!!
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info = {0};
dladdr(node->pc, &info);
// printf("%s \n",info.dli_sname);
NSString * name = @(info.dli_sname);
free(node);
//给函数添加_
BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
//是否去重??
[symbolNames addObject:symbolName];
/*
if ([name hasPrefix:@"+["]||[name hasPrefix:@"-["]) {
//如果是OC方法名称直接存!
[symbolNames addObject:name];
continue;
}
//如果不是OC直接加个_存!
[symbolNames addObject:[@"_" stringByAppendingString:name]];
*/
}
//反向数组
// symbolNames = (NSMutableArray<NSString *>*)[[symbolNames reverseObjectEnumerator] allObjects];
NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
//创建一个新数组
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
//去重!
while (name = [enumerator nextObject]) {
if (![funcs containsObject:name]) {//数组中不包含name
[funcs addObject:name];
}
}
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//数组转成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
//字符串写入文件
//文件路径
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
//文件内容
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
监控到方法个数。
我们生成的项目方法函数调用及顺序表:
- 只要添加Clang插桩标记,
编译器
会在所有方法,函数,block代码实现边缘添加一句代码__sanitizer_cov_trace_pc_guard
,代表其方法的调用
坑点
坑点
Clang在做代码跟踪时,不仅把方法,函数,block拦截,进入循环体内也进行了拦截,然后一直死循环,比如:touchesBegan
解决方式:
在Xcode->TARGETS->Build Settings的Other C Flags添加参数func设置仅拦截方法:
取反&去重
数组取反:
NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
去重:
//创建一个新数组
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
//去重!
while (name = [enumerator nextObject]) {
if (![funcs containsObject:name]) {//数组中不包含name
[funcs addObject:name];
}
}
生成.order文件
生成文件:
//文件路径
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
//文件内容
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
Swift方法处理(符号覆盖)
因为 Swift与OC不是同一个编译器。
创建添加.Swift文件,然后在OC代码中导入工程名- Swift.h
并build
Swift环境配置
Xcode->Build Setting -> Other Swift Flags(添加-sanitize-coverage=func
和-sanitize=undefined
)
然后编译:
- 看到Swift相应的类和方法,由于做了混淆,优化了性能。