App启动速度监控-方法级别启动耗时检查工具

本文是基于戴铭大佬的课程iOS开发高手课,加上个人实践+理解编写
本文已同步至掘金:App启动速度监控-方法级别启动耗时检查工具

如何做一个方法级别启动耗时检查工具来辅助分析和监控

使用hook objc_msgSend 方式来检查启动方法的执行耗时时,我们需要实现一个称手的启动时间检查工具

首先,需要了解为什么hookobjc_msgSend方法,就可以hook 全部Objective-C的方法

Objective-C里每个对象都会指向一个类,每个类都会有一个方法列表,方法列表里的每个方法都是由selector函数指针metadata组成的

objc_msgSend方法在运行时根据对象和方法的selector去找到对应的函数指针,然后执行。换句话说,objc_msgSendObjective-C里方法执行的必经之路,能够控制所有的Objective-C方法

objc_msgSend本身是用汇编语言写的,这样做的原因主要有两个:

  • objc_msgSend的调用频次最高,在它上面进行的性能优化能够提升整个App生命周期的性能。而汇编语言在性能优化上属于原子级优化,能够吧优化做到极致
  • 其他语言难以实现未知参数跳转到任意函数指针的功能

苹果开源了objective-c的运行时代码,可以在苹果开源网站找到objc_msgSend的源码

objc_msgSend 全架构实现源代码文件列表

上图列出的是所有架构的实现,包括x86_64等。objc_msgSend是iOS方法执行最核心的部门

objc_msgSend方法执行的逻辑是:先获取对象对应类的信息,再获取方法的缓存,根据方法的selector查找函数指针,经过异常错误处理后,最后跳转到对应函数的实现

hook objc_msgSend方法

Facebook开源了一个库,可以在iOS上运行的Mach-O二进制文件中动态的重新绑定符号,这个库叫fishhook : GitHub地址

fishhook实现的大致思路是,通过重新绑定符合,可以实现对c方法的hook。dyld是通过更新Mach-O二进制的_DATA segment特定的部分中的指针来绑定lazynon-lazy符号,通过确认传递给rebind_symbol里每个符号更新的位置,就可以找出对应替换来重新绑定这些符号。

fishhook的实现原理

首先,遍历dyld里的所有image, 取出image headerslide


        if (!_rebindings_head->next) {
            _dyld_register_func_for_add_image(_rebind_symbols_for_image);
        }else {
            uint32_t c = _dyld_image_count();
            //遍历所有image
            for (uint32_t i = 0; i < c; i++) {
                //读取 image header 和 slider
                _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
            }
        }

接下来,找到符号表相关的command,包括linkedit segment command、symtab command 和dysymtab command


    segment_command_t *cur_seg_cmd;
    segment_command_t *linkedit_segment = NULL;
    struct symtab_command * symtab_cmd = NULL;
    struct dysymtab_command *dysymtab_cmd = NULL;
    uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
    for (uint i = 0; i < header->ncmds; cur += cur_seg_cmd->cmdsize) {
        cur_seg_cmd = (segment_command_t *)cur;
        if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
            if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
                //linkedit segment command
                linkedit_segment = cur_seg_cmd;
            }
        }else if (cur_seg_cmd->cmd == LC_SYMTAB){
            //symtab command
            symtab_cmd = (struct symtab_command*)cur_seg_cmd;
        }else if (cur_seg_cmd->cmd == LC_DYSYMTAB){
            //dysymtab command
            dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
        }
    }

然后,获得baseindirect符号表:


   //找到base符号表地址
    uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
    nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
    char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
    //找到indirect符号表
    uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

最后,有了符号表和传入的方法替换数组,就可以进行符号表访问指针地址的替换了:


    uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    void **indirect_symbol_bindings = (void **)(uintptr_t)slide + section->addr);
    for (uint i = 0 ; i < section->size/sizeof(void *); i++) {
        uint32_t symtab_index = indirect_symbol_indices[I];
        if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
            continue;
        }
        
        uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
        char *symbol_name = strtab + strtab_offset;
        if (strnlen(symbol_name,2) < 2) {
            continue;
        }
        
        struct rebindings_entry *cur = rebindings;
        while (cur) {
            for (uint j = 0; j < cur->rebindings_nel; j++) {
                if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
                    if (cur->rebindings[j].replaced != NULL && indirect_symbol_bindings[i].replaced!= cur->rebindings[j].replacement) {
                        *(cur->rebindings[j].replaced) = indirect_symbol_bindings[I];
                    }
                    
                    //符号表访问指针地址的替换
                    indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
                    goto symbol_loop;
                }
            }
            cur = cur->next;
        }
    symbol_loop:;
        
    }

以上,就是fishhook的实现原理了,fishhook是对底层的操作,其中查找符号表的过程和堆栈符号化实现原理基本类似,了解其中原理对于理解可执行文件Mach-O内部结构会有很大的帮助。

接下来,我们再看一个问题:只靠fishhook 就能够搞定objc_msgSendhook了吗?

当然不够,objc_msgSend是用汇编语言实现的,所以我们还需要从汇编层多加点料

需要先实现两个方法pushCallRecordpopCallRecord,来分别记录objc_msgSend方法调用前后的时间,然后相减就能够得到方法的执行耗时。

下面针对arm64架构,编写一个可保留未知参数并跳转到c中任意函数指针的汇编代码,实现对objc_msgSend的hook。

arm643164 bit 的整数型寄存器,分别用x0x30表示,主要的实现思路是:

  1. 入栈参数,参数寄存器是x0~x07。对应objc_msgSend方法来说,x0第一个参数是传入对象,x1第二个参数是选择器_cmd, syscall的number会放到x8里。
  2. 交换寄存器中保存的参数,将用于返回的寄存器lr中的数据移到想x1里
  3. 使用 bl label 语法调用pushCallRecord函数
  4. 执行原始的objc_msgSend,保存返回值
  5. 使用bl label 语法调用popCallRecord函数

具体汇编代码如下:


static void replacementObjc_msgSend() {
  __asm__ volatile (
    // sp 是堆栈寄存器,存放栈的偏移地址,每次都指向栈顶。
    // 保存 {q0-q7} 偏移地址到 sp 寄存器
      "stp q6, q7, [sp, #-32]!\n"
      "stp q4, q5, [sp, #-32]!\n"
      "stp q2, q3, [sp, #-32]!\n"
      "stp q0, q1, [sp, #-32]!\n"
    // 保存 {x0-x8, lr}
      "stp x8, lr, [sp, #-16]!\n"
      "stp x6, x7, [sp, #-16]!\n"
      "stp x4, x5, [sp, #-16]!\n"
      "stp x2, x3, [sp, #-16]!\n"
      "stp x0, x1, [sp, #-16]!\n"
    // 交换参数.
      "mov x2, x1\n"
      "mov x1, lr\n"
      "mov x3, sp\n"
    // 调用 preObjc_msgSend,使用 bl label 语法。bl 执行一个分支链接操作,label 是无条件分支的,是和本指令的地址偏移,范围是 -128MB 到 +128MB
      "bl __Z15preObjc_msgSendP11objc_objectmP13objc_selectorP9RegState_\n"
      "mov x9, x0\n"
      "mov x10, x1\n"
      "tst x10, x10\n"
    // 读取 {x0-x8, lr} 从保存到 sp 栈顶的偏移地址读起
      "ldp x0, x1, [sp], #16\n"
      "ldp x2, x3, [sp], #16\n"
      "ldp x4, x5, [sp], #16\n"
      "ldp x6, x7, [sp], #16\n"
      "ldp x8, lr, [sp], #16\n"
    // 读取 {q0-q7}
      "ldp q0, q1, [sp], #32\n"
      "ldp q2, q3, [sp], #32\n"
      "ldp q4, q5, [sp], #32\n"
      "ldp q6, q7, [sp], #32\n"
      "b.eq Lpassthrough\n"
    // 调用原始 objc_msgSend。使用 blr xn 语法。blr 除了从指定寄存器读取新的 PC 值外效果和 bl 一样。xn 是通用寄存器的64位名称分支地址,范围是0到31
      "blr x9\n"
    // 保存 {x0-x9}
      "stp x0, x1, [sp, #-16]!\n"
      "stp x2, x3, [sp, #-16]!\n"
      "stp x4, x5, [sp, #-16]!\n"
      "stp x6, x7, [sp, #-16]!\n"
      "stp x8, x9, [sp, #-16]!\n"
    // 保存 {q0-q7}
      "stp q0, q1, [sp, #-32]!\n"
      "stp q2, q3, [sp, #-32]!\n"
      "stp q4, q5, [sp, #-32]!\n"
      "stp q6, q7, [sp, #-32]!\n"
    // 调用 postObjc_msgSend hook.
      "bl __Z16postObjc_msgSendv\n"
      "mov lr, x0\n"
    // 读取 {q0-q7}
      "ldp q6, q7, [sp], #32\n"
      "ldp q4, q5, [sp], #32\n"
      "ldp q2, q3, [sp], #32\n"
      "ldp q0, q1, [sp], #32\n"
    // 读取 {x0-x9}
      "ldp x8, x9, [sp], #16\n"
      "ldp x6, x7, [sp], #16\n"
      "ldp x4, x5, [sp], #16\n"
      "ldp x2, x3, [sp], #16\n"
      "ldp x0, x1, [sp], #16\n"
      "ret\n"
      "Lpassthrough:\n"
    // br 无条件分支到寄存器中的地址
      "br x9"
    );
}

现在,你就可以得到每个 Objective-C 方法的耗时了。接下来,我们再看看怎样才能够做到像下图那样记录和展示方法调用的层级关系和顺序呢?

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

推荐阅读更多精彩内容