其他分享
首页 > 其他分享> > iOS-底层原理 31:启动优化之二进制重排

iOS-底层原理 31:启动优化之二进制重排

作者:互联网

前提,在之前的两篇文章中,大致介绍了一些基本概念以及启动优化的思路,下面来着重介绍一个pre-main阶段的优化方案,即二进制重排,这个方案最开始是由于抖音的这篇文章抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%火起来的。

二进制重排原理

在虚拟内存部分,我们知道,当进程访问一个虚拟内存page,而对应的物理内存不存在时,会触发缺页中断(Page Fault),因此阻塞进程。此时就需要先加载数据到物理内存,然后再继续访问。这个对性能是有一定影响的。

基于Page Fault,我们思考,App在冷启动过程中,会有大量的类、分类、三方等需要加载和执行,此时的产生的Page Fault所带来的的耗时是很大的。以WeChat为例,我们来看下,在启动阶段的Page Fault的次数

@implementation ViewController

void test1(){
    printf("1");
}

void test2(){
    printf("2");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    test1();
}

+(void)load{
    printf("3");
    test2();
}
@end

从上面的Page Fault的次数以及加载顺序,可以发现其实导致Page Fault次数过多的根本原因是启动时刻需要调用的方法,处于不同的Page导致的。因此,我们的优化思路就是:将所有启动时刻需要调用的方法,排列在一起,即放在一个页中,这样就从多个Page Fault变成了一个Page Fault。这就是二进制重排的核心原理,如下所示

图片

二进制重排原理

注意:在iOS生产环境的app,在发生Page Fault进行重新加载时,iOS系统还会对其做一次签名验证,因此 iOS 生产环境的 Page Fault 比Debug环境下所产生的耗时更多。

二进制重排实践

下面,我们来进行具体的实践,首先理解几个名词

Link Map
Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File,Link Map主要包含三部分:

ld

ld是Xcode使用的链接器,有一个参数order_file,我们可以通过在Build Settings -> Order File配置一个后缀为order的文件路径。在这个order文件中,将所需要的符号按照顺序写在里面,在项目编译时,会按照这个文件的顺序进行加载,以此来达到我们的优化

所以二进制重排的本质就是对启动加载的符号进行重新排列

到目前为止,原理我们基本弄清楚了,如果项目比较小,完全可以自定义一个order文件,将方法的顺序手动添加,但是如果项目较大,涉及的方法特别多,此时我们如何获取启动运行的函数呢?有以下几种思路

Clang 插桩

llvm内置了一个简单的代码覆盖率检测(SanitizerCoverage)。它在函数级、基本块级和边缘级插入对用户定义函数的调用。我们这里的批量hook,就需要借助于SanitizerCoverage

关于 clang 的插桩覆盖的官方文档如下 : clang 自带代码覆盖工具 文档中有详细概述,以及简短Demo演示。

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
//原子队列,其目的是保证写入安全,线程安全
static  OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体,以链表的形式
typedef struct {
    void *pc;
    void *next;
}CJLNode;

/*
 - 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,并赋值
    CJLNode *node = malloc(sizeof(CJLNode));
    *node = (CJLNode){PC, NULL};
    
    //加入队列
    //符号的访问不是通过下标访问,是通过链表的next指针,所以需要借用offsetof(结构体类型,下一个的地址即next)
    OSAtomicEnqueue(&queue, node, offsetof(CJLNode, next));
}
extern void getOrderFile(void(^completion)(NSString *orderFilePath)){
    
    collectFinished = YES;
    __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<NSString *> *symbolNames = [NSMutableArray array];
        
        //while循环取符号
        while (YES) {
            //出队
            CJLNode *node = OSAtomicDequeue(&queue, offsetof(CJLNode, 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<NSString *> *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:@"cjl.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 {
    
    [self test11];
    
    getOrderFile(^(NSString *orderFilePath) {
        NSLog(@"OrderFilePath:%@", orderFilePath);
    });
    
    return YES;
}

- (void)test11{
    
}

此时的cjl.order中只有这三个方法

图片

cjl.order文件

注意点:避免死循环

解决方式:将BuildSetting中的other C Flags的-fsanitize-coverage=trace-pc-guard ,改成-fsanitize-coverage=func,trace-pc-guard

标签:符号,iOS,31,stop,地址,pc,guard,重排,Page
来源: https://blog.csdn.net/changcongcong_ios/article/details/119884538