抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%
1、二进制重排原理
当进程在访问虚拟内存时,如果对应的物理内存不存在,会触发缺页异常(pagefault)
,由于在启动的时候需要调用的方法存在不同类中,而每个page的大小是固定的,这就导致启动时需要加载的page会更多,我们可以通过手动排列符号,将启动时刻需要的方法排列在一起,减少缺页异常
查看没有优化前的方法编译顺序
- 自定义demo
@implementation ViewController
void test1(){
printf("1");
}
void test2(){
printf("2");
}
- (void)viewDidLoad {
[super viewDidLoad];
test1();
}
+(void)load{
printf("3");
test2();
}
@end
-
在
Build Setting --> Write Link Map File
设置YES
-
运行编译后,在对应的路径下(Path to Link Map File)找到
LinkMap
文件打开,我们可以通过替换方法位置重复改步骤发现,类里面函数的加载顺序是从上到下的
,通过替换Build Phases -- > Compile Sources
中文件的顺序,可以修改LinkMap
中文件的顺序
2、二进制重排
Link Map
LinkMap是iOS编译过程的中间产物,记录了二进制文件的布局
,通过在Xcode的Build Setting
中设置Write Link Map File = YES
开启,主要包含下面三个部分
-
Object Files
:生成二进制用到的link单元的路径和文件编号 -
Sections
:记录Mach-O文件中的每个Segment-section地址范围 -
Symbols
:按顺序记录每个符号的地址范围
ld
ld
是Xcode链接器,通过在Xcode的Build Setting --> Order File
中设置自定义的.order
后缀的文件路径,将需要重排的符号按顺序写在里面,当Xcode编译时会按照.order
文件中的符号顺序加载,我们可以通过下面几种方法获得APP启动时的运行函数
-
HOOK objc_msgSend
:由于objc_msgSend的参数是可变的,需要汇编获取,而且只能获取到OC
方法和Swift中的@objc
方法 -
静态扫描
:扫描Mach-O
文件中的 特定段和节里面所存储的符号以及函数数据 -
Clang插桩
:批量100%符号获取,OC、Swift、C
都可以获取
Clang插桩
通过LLVM内置的工具SanitizerCoverage
,可以在函数级、基本快级和边缘插入到用户定义函数的调用,官方文档clang 自带代码覆盖工具 中有使用简介和demo
【第一步】
- 开启
SanitizerCoverage
- oc项目中,
Build Settings --> Other C Flags
中添加-fsanitize-coverage=func,trace-pc-guard
- 注意:在官方demo中的是
-fsanitize-coverage=trace-pc-guard
在使用while循环时会出现死循环
- 注意:在官方demo中的是
- swift项目中,
Build Settings --> Other Swift Flags
中加入-sanitize-coverage=func
和-sanitize=undefined
- 也可以通过
Podfile
统一配置
- oc项目中,
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['OTHER_CFLAGS'] = '-fsanitize-coverage=func,trace-pc-guard'
config.build_settings['OTHER_SWIFT_FLAGS'] = '-sanitize-coverage=func -sanitize=undefined'
end
end
end
【第二步】
新建YPOrderFile
文件,重写下面两个方法
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop) {}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {}
-
__sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop)
方法记录了符号数量- 参数1
start
是一个指针,指向无符号的int类型,占4字节,相当于一个数组的起始位置,从高位往低位读取 - 参数2
stop
也是一个指针,因为数据是高位往低位读取,此时的&stop的地址并不是其真实地址,因为stop占了4个字节,所以stop真实地址=&stop-0x4
(类似我们在获取数组最后一个数据是需要减1一样),在项目中新增一个方法、block、c++时stop对应会加0x4,属性则会多0x12
- 参数1
-
__sanitizer_cov_trace_pc_guard (uint32_t *guard)
方法,捕获所有启动时刻的符号,将所有符号入队- 参数
guard
是一个哨兵,记录当前第几个被调用
- 参数
/原子队列,其目的是保证写入安全,线程安全
static OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体,以链表的形式
typedef struct {
void *pc;
void *next;
}YPNode;
/*
- start:起始位置
- stop:并不是最后一个符号的地址,而是整个符号表的最后一个地址,最后一个符号的地址=stop-4(因为是从高地址往低地址读取的,且stop是一个无符号int类型,占4个字节)。stop存储的值是符号的
*/
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p - %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++) {
*x = ++N;
}
}
/*
可以全面hook方法、函数、以及block调用,用于捕捉符号,是在多线程进行的,这个方法中只存储pc,以链表的形式
- guard 是一个哨兵,告诉我们是第几个被调用的
*/
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return;//将load方法过滤掉了,所以需要注释掉
//获取PC
/*
- PC 当前函数返回上一个调用的地址
- 0 当前这个函数地址,即当前函数的返回地址
- 1 当前函数调用者的地址,即上一个函数的返回地址
*/
void *PC = __builtin_return_address(0);
//创建node,并赋值
YPNode *node = malloc(sizeof(YPNode));
*node = (YPNode){PC, NULL};
//加入队列
//符号的访问不是通过下标访问,是通过链表的next指针,所以需要借用offsetof(结构体类型,下一个的地址即next)
OSAtomicEnqueue(&queue, node, offsetof(YPNode, next));
}
【第三步】
获取所有符号并写入文件保存
- 循环取出所有符号
- 数组取反,因为是入队存储是反序的
- 数组去重
- 符号保存到
yp.order
文件中
extern void getOrderFile(void(^completion)(NSString *orderFilePath)){
__sync_synchronize();
NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//创建符号数组
NSMutableArray *symbolNames = [NSMutableArray array];
//while循环取符号
while (YES) {
//出队
YPNode *node = OSAtomicDequeue(&queue, offsetof(YPNode, next));
if (node == NULL) break;
//取出PC,存入info
Dl_info info;
dladdr(node->pc, &info);
// printf("%s \n", info.dli_sname);
if (info.dli_sname) {
//判断是不是OC方法,如果不是,需要加下划线存储,反之,则直接存储
NSString *name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
}
if (symbolNames.count == 0) {
if (completion) {
completion(nil);
}
return;
}
//取反(队列的存储是反序的)
NSEnumerator *emt = [symbolNames reverseObjectEnumerator];
//去重
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
while (name = [emt nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//去掉自己
[funcs removeObject:functionExclude];
//将数组变成字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
NSLog(@"Order:\n%@", funcStr);
//字符串写入文件
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"yp.order"];
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
if (completion) {
completion(success ? filePath : nil);
}
});
}
【第四步】
在合适的地方调用方法
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
getOrderFile(^(NSString *orderFilePath) {
NSLog(@"OrderFilePath:%@", orderFilePath);
});
return YES;
}
【第五步】
将yp.order
文件拷贝,放入主目录路径中,并在Build Settings --> Order File
中配./yp.order
,也可以放在别的目录,只要在order File中配置对应.order文件的路径即可
完整文件
#import "YPOrderFile.h"
#include
#include
#include
#import
#import
@implementation YPOrderFile
//原子队列,其目的是保证写入安全,线程安全
static OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体,以链表的形式
typedef struct {
void *pc;
void *next;
}YPNode;
/*
- start:起始位置
- stop:并不是最后一个符号的地址,而是整个符号表的最后一个地址,最后一个符号的地址=stop-4(因为是从高地址往低地址读取的,且stop是一个无符号int类型,占4个字节)。stop存储的值是符号的
*/
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p - %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++) {
*x = ++N;
}
}
/*
可以全面hook方法、函数、以及block调用,用于捕捉符号,是在多线程进行的,这个方法中只存储pc,以链表的形式
- guard 是一个哨兵,告诉我们是第几个被调用的
*/
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return;//将load方法过滤掉了,所以需要注释掉
//获取PC
/*
- PC 当前函数返回上一个调用的地址
- 0 当前这个函数地址,即当前函数的返回地址
- 1 当前函数调用者的地址,即上一个函数的返回地址
*/
void *PC = __builtin_return_address(0);
//创建node,并赋值
YPNode *node = malloc(sizeof(YPNode));
*node = (YPNode){PC, NULL};
//加入队列
//符号的访问不是通过下标访问,是通过链表的next指针,所以需要借用offsetof(结构体类型,下一个的地址即next)
OSAtomicEnqueue(&queue, node, offsetof(YPNode, next));
}
extern void getOrderFile(void(^completion)(NSString *orderFilePath)){
__sync_synchronize();
NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//创建符号数组
NSMutableArray *symbolNames = [NSMutableArray array];
//while循环取符号
while (YES) {
//出队
YPNode *node = OSAtomicDequeue(&queue, offsetof(YPNode, next));
if (node == NULL) break;
//取出PC,存入info
Dl_info info;
dladdr(node->pc, &info);
// printf("%s \n", info.dli_sname);
if (info.dli_sname) {
//判断是不是OC方法,如果不是,需要加下划线存储,反之,则直接存储
NSString *name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
}
if (symbolNames.count == 0) {
if (completion) {
completion(nil);
}
return;
}
//取反(队列的存储是反序的)
NSEnumerator *emt = [symbolNames reverseObjectEnumerator];
//去重
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
while (name = [emt nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//去掉自己
[funcs removeObject:functionExclude];
//将数组变成字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
NSLog(@"Order:\n%@", funcStr);
//字符串写入文件
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"yp.order"];
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
if (completion) {
completion(success ? filePath : nil);
}
});
}
@end