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

我们都知道二进制重排能减少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的耗时优化。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容