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的源码


image.png


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里。


  1. 交换寄存器中保存的参数,将用于返回的寄存器lr中的数据移到想x1里


  1. 使用 bl label 语法调用pushCallRecord函数


  1. 执行原始的objc_msgSend,保存返回值


  1. 使用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 方法的耗时了。接下来,我们再看看怎样才能够做到像下图那样记录和展示方法调用的层级关系和顺序呢?


image.png

方法调用层级和顺序


相关文章
|
4月前
|
Linux Shell 网络安全
【Azure 应用服务】如何来检查App Service上证书的完整性以及在实例中如何查找证书是否存在呢?
【Azure 应用服务】如何来检查App Service上证书的完整性以及在实例中如何查找证书是否存在呢?
家政服务小程序APP开发,做好上门家政最快的方法是什么?
在家政服务领域,打造成功的平台并非易事。本文分享了三个关键步骤:避免初期盲目投入、采用低成本获客方式、建立有效的阿姨筛选机制。遵循这些方法,可助你避开常见陷阱,成为行业头部平台。
|
3月前
|
测试技术
基于LangChain手工测试用例转App自动化测试生成工具
在传统App自动化测试中,测试工程师需手动将功能测试用例转化为自动化用例。市面上多数产品通过录制操作生成测试用例,但可维护性差。本文探讨了利用大模型直接生成自动化测试用例的可能性,介绍了如何使用LangChain将功能测试用例转换为App自动化测试用例,大幅节省人力与资源。通过封装App底层工具并与大模型结合,记录执行步骤并生成自动化测试代码,最终实现高效自动化的测试流程。
|
4月前
|
Python
【Azure 应用服务】App Service的运行状况检查功能失效,一直提示"实例运行不正常"
【Azure 应用服务】App Service的运行状况检查功能失效,一直提示"实例运行不正常"
|
4月前
【Azure App Service】如何来停止 App Service 的高级工具站点 Kudu ?
【Azure App Service】如何来停止 App Service 的高级工具站点 Kudu ?
|
4月前
|
PHP 开发工具 git
【Azure 应用服务】在 App Service for Windows 中自定义 PHP 版本的方法
【Azure 应用服务】在 App Service for Windows 中自定义 PHP 版本的方法
|
4月前
|
安全 网络安全 Windows
【Azure App Service】遇见az命令访问HTTPS App Service 时遇见SSL证书问题,暂时跳过证书检查的办法
【Azure App Service】遇见az命令访问HTTPS App Service 时遇见SSL证书问题,暂时跳过证书检查的办法
【Azure App Service】遇见az命令访问HTTPS App Service 时遇见SSL证书问题,暂时跳过证书检查的办法
|
4月前
|
安全 Java 应用服务中间件
【Azure 应用服务】App Service中,为Java应用配置自定义错误页面,禁用DELETE, PUT方法
【Azure 应用服务】App Service中,为Java应用配置自定义错误页面,禁用DELETE, PUT方法
【Azure 应用服务】App Service中,为Java应用配置自定义错误页面,禁用DELETE, PUT方法
|
4月前
|
iOS开发
App备案与iOS云管理式证书 ,公钥及证书SHA-1指纹的获取方法
App备案与iOS云管理式证书 ,公钥及证书SHA-1指纹的获取方法
238 0
App备案与iOS云管理式证书 ,公钥及证书SHA-1指纹的获取方法
|
4月前
|
XML 安全 Java
App安全检测实践基础——工具
App安全检测实践基础——工具
116 0