美文网首页iOS性能调优、测试性能优化
iOS App启动时间优化--Clang插桩获取启动调用的函数符

iOS App启动时间优化--Clang插桩获取启动调用的函数符

作者: jayhe | 来源:发表于2020-04-24 14:49 被阅读0次

    我们都知道二进制重排能减少PageFault是次数,从而减少一部分启动时间;那么关键是如何获取启动都调用了哪些函数了

    获取启动执行了哪些方法

    Objective C方法

    绝大部分OC的方法可以通过hook objc_msg_send来获取到

    C++静态初始化

    C++并不像Objective C方法那样,大部分方法调用编译后都是objc_msgSend,也就没有一个入口函数去运行时hook。
    但是可以用-finstrument-functions在编译期插桩“hook”,或者使用并不完美但成本最低的静态扫描方案。

    Block

    Block在编译后的函数体是一个C函数,在调用的时候直接通过void *invoke;指针调用,并不走objc_msgSend,所以需要单独hook。

    以上的实现方式摘抄自抖音的二进制重排的手段,也是常用的手段
    抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%
    不得不说抖音团队的实力还是强啊,通过二进制重排去提升启动时间的瓶颈

    Swift函数

    Clang插桩可以,其他的方式暂时不知道,不会Swift

    优雅的方式-Clang插桩
    Clang官方文档
    先看看效果吧:

    • c函数


      图片.png
    • oc方法


      图片.png
    • Block


      图片.png
    • Swift方法


      图片.png

    可以看到,我们使用插桩的方式,当调用了OC C Block Swift方法时,都会调用插桩的函数

    Clang插桩实践

    原理简述

    图片.png
    1. LLVM支持我们在添加编译选项-fsanitize-coverage=trace-pc-guard的时候,编译时帮我们在函数中插入__sanitizer_cov_trace_pc_guard,从上面的截图的汇编代码可以看到,当函数调用的时候,会callq__sanitizer_cov_trace_pc_guard

    2. 利用__builtin_return_address(0)来获得当前函数返回地址,也就是调用方的地址。

    3. 通过dladdr来将指针解析成Dl_info结构体信息,其中dli_sname就是符号的名称

    typedef struct dl_info {
            const char      *dli_fname;     /* Pathname of shared object */
            void            *dli_fbase;     /* Base address of shared object */
            const char      *dli_sname;     /* Name of nearest symbol */
            void            *dli_saddr;     /* Address of nearest symbol */
    } Dl_info;
    

    集成工具

    pod 'HCClangTrace', :git => 'https://github.com/jayhe/HCClangTrace.git'

    核心实现代码如下:

    
    // The guards are [start, stop).
    // This function will be called at least once per DSO and may be called
    // more than once with the same values of start/stop.
    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.
    }
    
    //原子队列
    static  OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
    //定义符号结构体
    typedef struct {
        void *pc;
        void *next;
    } SymbolNode;
    
    // This callback is inserted by the compiler as a module constructor
    // into every DSO. 'start' and 'stop' correspond to the
    // beginning and end of the section with the guards for the entire
    // binary (executable or DSO). The callback will be called at least
    // once per DSO and may be called multiple times with the same parameters.
    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
        if (!*guard) return; // Duplicate the guard check.
        // If you set *guard to 0 this code will not be called again for this edge.
        // Now you can get the PC and do whatever you want:
        //   store it somewhere or symbolize it and print right away.
        // The values of `*guard` are as you set them in
        // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
        // and use them to dereference an array or a bit vector.
        void *PC = __builtin_return_address(0);
        SymbolNode *node = malloc(sizeof(SymbolNode));
        *node = (SymbolNode){PC, NULL};
        //进入
        OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
        /*
        printf("fname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n",
               info.dli_fname,
               info.dli_fbase,
               info.dli_sname,
               info.dli_saddr);
         */
    }
    
    + (void)generateOrderFile {
        NSMutableArray <NSString *> *symbolNames = [NSMutableArray array];
        while (YES) {
            SymbolNode * node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
            if (node == NULL) {
                break;
            }
            Dl_info info;
            dladdr(node->pc, &info);
            NSString *name = @(info.dli_sname);
            // 判断是不是oc方法,是的话直接加入符号数组
            BOOL isInstanceMethod = [name hasPrefix:@"-["];
            BOOL isClassMethod = [name hasPrefix:@"+["];
            BOOL isObjc = isInstanceMethod || isClassMethod;
            /* 非oc方法,一般会加上一个'_',这是由于UNIX下的C语言规定全局的变量和函数经过编译后会在符号前加下划线从而减少多种语言目标文件之间的符号冲突的概率;可以通过编译选项'-fleading-underscore'开启、'-fno-leading-underscore'来关闭
             */
            NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
            [symbolNames addObject:symbolName];
        }
        // 取反:将先调用的函数放到前面
        NSEnumerator * emt = [symbolNames reverseObjectEnumerator];
        // 去重:由于一个函数可能执行多次,__sanitizer_cov_trace_pc_guard会执行多次,就加了重复的了,所以去重一下
        NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
        NSString *name;
        while (name = [emt nextObject]) {
            if (![funcs containsObject:name]) {
                [funcs addObject:name];
            }
        }
        // 由于trace了所有执行的函数,这里我们就把本函数移除掉
        [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
        // 写order文件
        NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
        NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"trace.order"];
        NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
        [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    #if DEBUG
        NSLog(@"%@",funcStr);
    #endif
    }
    

    修改配置
    在主项目Target--Build Settings中添加编译选项
    Other C Flags增加-fsanitize-coverage=func,trace-pc-guard

    图片.png

    如果你是OC Swift混编,则在Other Swift Flags增加-sanitize-coverage=func,-sanitize=undefined

    图片.png

    小技巧:
    如果你有使用的是pod管理代码,用的是静态库的方式的话,那么可以通过hook来修改所有的pod库的编译选项;将以下内容拷贝到你的Podfile文件下面,执行pod install --no-repo-update

    post_install do |installer|
      installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
          macho_type = config.build_settings['MACH_O_TYPE']
          #if macho_type == 'staticlib'
            # 将依赖的pod项目的Other C Flags加上’-fsanitize-coverage=func,trace-pc-guard‘选项
            config.build_settings['OTHER_CFLAGS'] ||= ['$(inherited)', '-fsanitize-coverage=func,trace-pc-guard']
            config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)', '-fsanitize-coverage=func,trace-pc-guard']
          #end
        end
      end
    end
    

    采集程序启动执行方法orderfile

    在你的首页的viewDidAppear函数中加上生成orderFile的函数,然后运行app

    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        [HCClangTrace generateOrderFile];
    }
    

    会在app的沙盒的tmp目录下生成,trace.order的文件;我的测试项目采集的符号如下:


    图片.png

    二进制重排

    将上一步生成的order文件的内容拷贝到你的order文件(如果你已创建),或者直接使用这个生成的trace.order文件

    具体Xcode中如何配置就不赘述了,参照这个来设置就好
    iOS App启动时间优化 二进制重排和PGO
    设置好order file之后,运行项目,查看linkmap

    图片.png

    二进制已经按照我们的order设置重新排列了。

    结束语

    • 这篇文章,主要讲了怎么用。原理这些大家可以交流交流。
    • 二进制重排主要是将启动执行的方法放到一起(按页)减少PageFaults次数,往往是打破启动时间优化瓶颈采用的手段,一般的项目建议还是先关注main之后的耗时优化,再来关注pre-main的耗时优化。

    相关文章

      网友评论

        本文标题:iOS App启动时间优化--Clang插桩获取启动调用的函数符

        本文链接:https://www.haomeiwen.com/subject/bbrfwhtx.html