iOS 启动优化--二进制重排

抖音研发实践:基于二进制文件重排的解决方案 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循环时会出现死循环
        SanitizerCoverage
    • swift项目中,Build Settings --> Other Swift Flags中加入-sanitize-coverage=func-sanitize=undefined
      SanitizerCoverage
    • 也可以通过Podfile统一配置
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)方法记录了符号数量

    • 参数1start是一个指针,指向无符号的int类型,占4字节,相当于一个数组的起始位置,从高位往低位读取
    • 参数2stop也是一个指针,因为数据是高位往低位读取,此时的&stop的地址并不是其真实地址,因为stop占了4个字节,所以stop真实地址=&stop-0x4(类似我们在获取数组最后一个数据是需要减1一样),在项目中新增一个方法、block、c++时stop对应会加0x4,属性则会多0x12
  • __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

你可能感兴趣的:(iOS 启动优化--二进制重排)