iOS 如何抓取线程的“方法调用栈”?

avatar
奇舞团移动端团队 @奇舞团

级别:★★☆☆☆
标签:「方法调用栈」「抓取线程的“方法调用栈”」
作者: 647
审校: QiShare团队


场景:
在一些 “性能监控” 的工具中,在检测到App主线程卡顿的时候,可以通过子线程抓取当前时刻所有线程的方法调用堆栈(保存卡顿现场),并在合适的时机(WiFi环境&网络环境较好的时候)把堆栈信息上传到我们的服务端。服务端将堆栈信息过滤分析后,交给客户端做优化处理。 这样,就能较好的提高用户的体验,并及时发现线上环境下的问题。
同时,也可以及时发现问题,及时优化我们的代码质量和执行效率。
(一个比较好的开发循环)


call stack

那么,在App发生卡顿时候,我们该如何抓取方法调用栈呢?堆栈信息又是什么样的呢?
本文将通过一个具体的 demo ,阐述如何进行抓栈操作。

在此之前,首先要感谢我偶像bestswifter的博客:《获取任意线程调用栈的那些事》,对我有很大的启发与帮助。

接下来,进入我们今天的正题:

  1. 什么是调用栈?
  2. 如何抓取线程当前的调用栈?
  3. 如何符号化解析?
  4. 一些特殊的调用栈
  5. (补充)如何检测App卡顿?

一、什么是调用栈?

调用栈(call stack):
是计算机科学中存储有关正在运行的子程序的消息的栈。—— 维基百科

在我们程序运行中,通常存在一个函数调用另一个函数的情况。
例如,在某个线程中,调用了 func A。在 func A 执行过程中,调用了 func B

那么,在计算机程序底层需要做哪些事呢?

  1. 转移控制 :暂停 func A ,并开始执行 func B,并在 func B执行完后,再回到 func A 继续执行。
  2. 转移数据func A 要能把参数传递给 func B,并且 func B如果有返回值的话,要把返回值还给 func A
  3. 分配和释放内存 :在 func B 开始执行时,给需要用到局部变量分配内存。在 func B 执行完后,释放这部分内存。

举个例子, 我声明了两个函数:foobar。 同时,在函数foo中调用了函数bar

- (void)foo {
    [self bar];
}

- (void)bar {
    NSLog(@"QiShare");
}

在模拟器(x86)下,会转换成如下汇编:

QiStackFrameLogger`-[ViewController foo]:
    0x105a1f0d0 <+0>:  pushq  %rbp
    0x105a1f0d1 <+1>:  movq   %rsp, %rbp
    0x105a1f0d4 <+4>:  subq   $0x10, %rsp
    0x105a1f0d8 <+8>:  movq   %rdi, -0x8(%rbp)
    0x105a1f0dc <+12>: movq   %rsi, -0x10(%rbp)
    0x105a1f0e0 <+16>: movq   -0x8(%rbp), %rax
    0x105a1f0e4 <+20>: movq   0x64a5(%rip), %rsi        ; "bar"
    0x105a1f0eb <+27>: movq   %rax, %rdi
    0x105a1f0ee <+30>: callq  *0x3f1c(%rip)             ; (void *)0x00007fff50ad3400: objc_msgSend
->  0x105a1f0f4 <+36>: addq   $0x10, %rsp
    0x105a1f0f8 <+40>: popq   %rbp
    0x105a1f0f9 <+41>: retq   
QiStackFrameLogger`-[ViewController bar]:
    0x105a1f100 <+0>:  pushq  %rbp
    0x105a1f101 <+1>:  movq   %rsp, %rbp
    0x105a1f104 <+4>:  subq   $0x10, %rsp
    0x105a1f108 <+8>:  leaq   0x3f61(%rip), %rax        ; @"QiShare"
    0x105a1f10f <+15>: movq   %rdi, -0x8(%rbp)
    0x105a1f113 <+19>: movq   %rsi, -0x10(%rbp)
->  0x105a1f117 <+23>: movq   %rax, %rdi
    0x105a1f11a <+26>: movb   $0x0, %al
    0x105a1f11c <+28>: callq  0x105a20cd4               ; symbol stub for: NSLog
    0x105a1f121 <+33>: jmp    0x105a1f121               ; <+33> at ViewController.m:24:5

在我的真机(arm64)下,会转换成如下汇编:

QiStackFrameLogger`-[ViewController foo]:
    0x10443833c <+0>:  sub    sp, sp, #0x20             ; =0x20 
    0x104438340 <+4>:  stp    x29, x30, [sp, #0x10]
    0x104438344 <+8>:  add    x29, sp, #0x10            ; =0x10 
    0x104438348 <+12>: adrp   x8, 9
    0x10443834c <+16>: add    x8, x8, #0x5a8            ; =0x5a8 
    0x104438350 <+20>: str    x0, [sp, #0x8]
    0x104438354 <+24>: str    x1, [sp]
    0x104438358 <+28>: ldr    x9, [sp, #0x8]
    0x10443835c <+32>: ldr    x1, [x8]
    0x104438360 <+36>: mov    x0, x9
    0x104438364 <+40>: bl     0x10443a0ac               ; symbol stub for: objc_msgSend
->  0x104438368 <+44>: ldp    x29, x30, [sp, #0x10]
    0x10443836c <+48>: add    sp, sp, #0x20             ; =0x20 
    0x104438370 <+52>: ret    
QiStackFrameLogger`-[ViewController bar]:
    0x104438374 <+0>:  sub    sp, sp, #0x20             ; =0x20 
    0x104438378 <+4>:  stp    x29, x30, [sp, #0x10]
    0x10443837c <+8>:  add    x29, sp, #0x10            ; =0x10 
    0x104438380 <+12>: str    x0, [sp, #0x8]
    0x104438384 <+16>: str    x1, [sp]
->  0x104438388 <+20>: adrp   x0, 4
    0x10443838c <+24>: add    x0, x0, #0x58             ; =0x58 
    0x104438390 <+28>: bl     0x104439fe0               ; symbol stub for: NSLog
    0x104438394 <+32>: b      0x104438394               ; <+32> at ViewController.m:24:5

再转换成更直观的图解,就变成了这样:

image

目前,绝大部分iOS设备都是基于arm64架构的(iPhone5s及之后发布的所有设备)。
通过查询 arm的官方文档,我们可以得知:

地址 名称 作用
sp 栈指针(stack pointer) 存放当前函数的地址。
x30 链接寄存器(link register) 存储函数的返回地址。
x29 帧指针寄存器(frame pointer) 上一级函数的地址(与x30一致)。
x19~x28 Callee-saved registers 被调用这保存寄存器。
x18 The Platform Register 平台保留,操作系统自身使用。
x17、x16 Intra-procedure-call temporary registers 临时寄存器。
x9~x15 Temporary registers 临时寄存器,用来保存本地变量。
x8 Indirect result location register 间接返回地址,返回地址过大时使用。
x0~x7 Parameter/result registers 参数/返回值寄存器。

其中,比较重要的是栈指针(stack pointer,下面简称sp)与帧指针(frame pointer,下面简称fp)。
sp会存储当前函数的栈顶地址,fp会存储上一级函数的sp


二、如何抓取线程当前的调用栈?

刚才,我们已经知道了通过fp就能找到上一级函数的地址。
通过不停的找上一级fp就能找到当前所有方法调用栈的地址。(回溯法)

Talk is easy, show me code.

  • 第一步:
    首先,我们声明一个结构体,用来存储链式的栈指针信息。(sp+fp
// 栈帧结构体:
typedef struct QiStackFrameEntry {
    const struct QiStackFrameEntry *const previouts; //!< 上一个栈帧
    const uintptr_t return_address;                  //!< 当前栈帧的地址
} QiStackFrameEntry;

没错,是个链表。

  • 第二步:
    取出 thread 里的 machine context
_STRUCT_MCONTEXT machineContext; // 先声明一个context,再从thread中取出context
if(![self qi_fillThreadStateFrom:thread intoMachineContext:&machineContext]) {
    return [NSString stringWithFormat:@"Fail to get machineContext from thread: %u\n", thread];
}

具体实现:

/*!
 @brief 将machineContext从thread中提取出来
 @param thread 当前线程
 @param machineContext 所要赋值的machineContext
 @return 是否获取成功
 */
+ (BOOL) qi_fillThreadStateFrom:(thread_t) thread intoMachineContext:(_STRUCT_MCONTEXT *)machineContext {
    mach_msg_type_number_t state_count = Qi_THREAD_STATE_COUNT;
    kern_return_t kr = thread_get_state(thread, Qi_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
    return kr == KERN_SUCCESS;
}
  • 第三步:
    获取machineContext里,在栈帧的指针地址。
    再通过fp的回溯,将所有的方法地址保存在backtraceBuffer数组中。
    直到找到最底层,没有上一级地址就break
uintptr_t backtraceBuffer[50];
int i = 0;
NSMutableString *resultString = [[NSMutableString alloc] initWithFormat:@"Backtrace of Thread %u:\n", thread];

const uintptr_t instructionAddress = qi_mach_instructionAddress(&machineContext);
backtraceBuffer[i++] = instructionAddress;

uintptr_t linkRegister = qi_mach_linkRegister(&machineContext);
if (linkRegister) {
    backtraceBuffer[i++] = linkRegister;
}

if (instructionAddress == 0) {
    return @"Fail to get instructionAddress.";
}

QiStackFrameEntry frame = {0};
const uintptr_t framePointer = qi_mach_framePointer(&machineContext);
if (framePointer == 0 || qi_mach_copyMem((void *)framePointer, &frame, sizeof(frame)) != KERN_SUCCESS) {
    return @"Fail to get frame pointer";
}

// 对frame进行赋值
for (; i<50; i++) {
    backtraceBuffer[i] = frame.return_address; // 把当前的地址保存
    if (backtraceBuffer[i] == 0 || frame.previouts == 0 || qi_mach_copyMem(frame.previouts, &frame, sizeof(frame)) != KERN_SUCCESS) {
        break; // 找到原始帧,就break
    }
}

这样,backtraceBuffer这个数组中,就存了当前时刻线程的方法调用地址(fp的集合)

backtraceBuffer这个数组,目前只是一堆方法的地址。
我们并不知道它具体指的是哪个方法?

那就需要接下来的 “符号化解析” 操作。
将每个地址与对应符号名(函数/方法名)一一对应上。


三、如何符号化解析?

我们通过回溯帧指针(fp),就能拿到线程下的所有函数调用地址。
我们怎么把地址与对应的符号(函数/方法名)对应上呢?

这就需要符号化解析步骤。
符号化解析:“地址” => “符号”

  • 预备:
    这次不用我们自己声明了,系统帮我们准备好了结构体dl_info
    专门用来存储当前的符号信息。
/*
 * Structure filled in by dladdr().
 */
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;
  • 第一步:
    根据backtraceBuffer数组的大小,声明一个同样大小的dl_info[]数组来存符号信息。
int backtraceLength = i;
Dl_info symbolicated[backtraceLength];
qi_symbolicate(backtraceBuffer, symbolicated, backtraceLength, 0); //!< 符号化
  • 第二步:
    通过address找到符号所在的image
    下面的方法,可以拿到对应imageindex(编号)。
// 找出address所对应的image编号
uint32_t qi_getImageIndexContainingAddress(const uintptr_t address) {
    const uint32_t imageCount = _dyld_image_count(); // dyld中image的个数
    const struct mach_header *header = 0;
    
    for (uint32_t i = 0; i < imageCount; i++) {
        header = _dyld_get_image_header(i);
        if (header != NULL) {
            // 在提供的address范围内,寻找segment command
            uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(i); //!< ASLR
            uintptr_t cmdPointer = qi_firstCmdAfterHeader(header);
            if (cmdPointer == 0) {
                continue;
            }
            for (uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
                const struct load_command *loadCmd = (struct load_command*)cmdPointer;
                if (loadCmd->cmd == LC_SEGMENT) {
                    const struct segment_command *segCmd = (struct segment_command*)cmdPointer;
                    if (addressWSlide >= segCmd->vmaddr && addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 命中!
                        return i;
                    }
                }
                else if (loadCmd->cmd == LC_SEGMENT_64) {
                    const struct segment_command_64 *segCmd = (struct segment_command_64*)cmdPointer;
                    if (addressWSlide >= segCmd->vmaddr && addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 命中!
                        return i;
                    }
                }
                cmdPointer += loadCmd->cmdsize;
            }
        }
    }
    
    return UINT_MAX; // 没找到就返回UINT_MAX
}
  • 第三步:
    我们拿到了address所对应的imageindex
    我们就可以通过一些系统方法与计算,得到header、虚拟内存地址、ASLR偏移量(安全性考虑,为了防黑客入侵。iOS 5Android 4后引入)。
    以及,比较关键的segmentBase(通过 baseAddress + ASLR 得到)。
const struct mach_header *header = _dyld_get_image_header(index); // 根据index找到header
const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(index); //image虚拟内存地址
const uintptr_t addressWithSlide = address - imageVMAddrSlide; // ASLR偏移量
const uintptr_t segmentBase = qi_getSegmentBaseAddressOfImageIndex(index) + imageVMAddrSlide; // segmentBase是根据index + ASLR得到的
if (segmentBase == 0) {
    return false;
}

info->dli_fname = _dyld_get_image_name(index);
info->dli_fbase = (void *)header;
  • 第四步:
    通过查找符号表,找到对应的符号,并赋值给dl_info数组。
// 查找符号表,找到对应的符号
const Qi_NLIST* bestMatch = NULL;
uintptr_t bestDistace = ULONG_MAX;
uintptr_t cmdPointer = qi_firstCmdAfterHeader(header);
if (cmdPointer == 0) {
    return false;
}
for (uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
    const struct load_command* loadCmd = (struct load_command*)cmdPointer;
    if (loadCmd->cmd == LC_SYMTAB) {
        const struct symtab_command *symtabCmd = (struct symtab_command*)cmdPointer;
        const Qi_NLIST* symbolTable = (Qi_NLIST*)(segmentBase + symtabCmd->symoff);
        const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
        
        /*
         *
         struct symtab_command {
             uint32_t    cmd;        / LC_SYMTAB /
             uint32_t    cmdsize;    / sizeof(struct symtab_command) /
             uint32_t    symoff;     / symbol table offset 符号表偏移 /
             uint32_t    nsyms;      / number of symbol table entries 符号表条目的数量 /
             uint32_t    stroff;     / string table offset 字符串表偏移 /
             uint32_t    strsize;    / string table size in bytes 字符串表的大小(以字节为单位) /
         };
         */
        
        for (uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
            // 如果n_value为0,则该符号引用一个外部对象。
            if (symbolTable[iSym].n_value != 0) {
                uintptr_t symbolBase = symbolTable[iSym].n_value;
                uintptr_t currentDistance = addressWithSlide - symbolBase;
                if ((addressWithSlide >= symbolBase) && (currentDistance <= bestDistace)) {
                    bestMatch = symbolTable + iSym;
                    bestDistace = currentDistance;
                }
            }
        }
        if (bestMatch != NULL) {
            info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
            info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
            if (*info->dli_sname == '_') {
                info->dli_sname++;
            }
            //如果所有的符号都被删除,就会发生这种情况。
            if (info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                info->dli_sname = NULL;
            }
            break;
        }
    }
    cmdPointer += loadCmd->cmdsize;
}
  • 第五步:
    遍历backtraceBuffer数组,并把符号信息赋值dl_info数组。
// 符号化:将backtraceBuffer(地址数组)转成symbolsBuffer(符号数组)。
void qi_symbolicate(const uintptr_t* const backtraceBuffer,
                    Dl_info* const symbolsBuffer,
                    const int numEntries,
                    const int skippedEntries) {
    int i = 0;
    
    if(!skippedEntries && i < numEntries) {
        qi_dladdr(backtraceBuffer[i], &symbolsBuffer[i]);
        i++;
    }
    
    for (; i < numEntries; i++) {
        qi_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]), &symbolsBuffer[i]); //!< 通过回溯得到的栈帧,找到对应的符号名。
    }
}
  • 小结:
    符号化解析,完整代码如下:
#pragma mark - Symbolicate

// 符号化:将backtraceBuffer(地址数组)转成symbolsBuffer(符号数组)。
void qi_symbolicate(const uintptr_t* const backtraceBuffer,
                    Dl_info* const symbolsBuffer,
                    const int numEntries,
                    const int skippedEntries) {
    int i = 0;
    
    if(!skippedEntries && i < numEntries) {
        qi_dladdr(backtraceBuffer[i], &symbolsBuffer[i]);
        i++;
    }
    
    for (; i < numEntries; i++) {
        qi_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]), &symbolsBuffer[i]); //!< 通过回溯得到的栈帧,找到对应的符号名。
    }
}

// 通过address得到当前函数info信息,包括:dli_fname、dli_fbase、dli_saddr、dli_sname.
bool qi_dladdr(const uintptr_t address, Dl_info* const info) {
    info->dli_fname = NULL;
    info->dli_fbase = NULL;
    info->dli_saddr = NULL;
    info->dli_sname = NULL;
    
    const uint32_t index = qi_getImageIndexContainingAddress(address); // 根据地址找到image中的index。
    if (index == UINT_MAX) {
        return false; // 没找到就返回UINT_MAX
    }
    
    /*
     Header
     ------------------
     Load commands
     Segment command 1 -------------|
     Segment command 2              |
     ------------------             |
     Data                           |
     Section 1 data |segment 1 <----|
     Section 2 data |          <----|
     Section 3 data |          <----|
     Section 4 data |segment 2
     Section 5 data |
     ...            |
     Section n data |
     */
    /*----------Mach Header---------*/
    const struct mach_header *header = _dyld_get_image_header(index); // 根据index找到header
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(index); //image虚拟内存地址
    const uintptr_t addressWithSlide = address - imageVMAddrSlide; // ASLR偏移量
    const uintptr_t segmentBase = qi_getSegmentBaseAddressOfImageIndex(index) + imageVMAddrSlide; // segmentBase是根据index + ASLR得到的
    if (segmentBase == 0) {
        return false;
    }
    
    info->dli_fname = _dyld_get_image_name(index);
    info->dli_fbase = (void *)header;
    
    // 查找符号表,找到对应的符号
    const Qi_NLIST* bestMatch = NULL;
    uintptr_t bestDistace = ULONG_MAX;
    uintptr_t cmdPointer = qi_firstCmdAfterHeader(header);
    if (cmdPointer == 0) {
        return false;
    }
    for (uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
        const struct load_command* loadCmd = (struct load_command*)cmdPointer;
        if (loadCmd->cmd == LC_SYMTAB) {
            const struct symtab_command *symtabCmd = (struct symtab_command*)cmdPointer;
            const Qi_NLIST* symbolTable = (Qi_NLIST*)(segmentBase + symtabCmd->symoff);
            const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
            
            /*
             *
             struct symtab_command {
                 uint32_t    cmd;        / LC_SYMTAB /
                 uint32_t    cmdsize;    / sizeof(struct symtab_command) /
                 uint32_t    symoff;     / symbol table offset 符号表偏移 /
                 uint32_t    nsyms;      / number of symbol table entries 符号表条目的数量 /
                 uint32_t    stroff;     / string table offset 字符串表偏移 /
                 uint32_t    strsize;    / string table size in bytes 字符串表的大小(以字节为单位) /
             };
             */
            
            for (uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
                // 如果n_value为0,则该符号引用一个外部对象。
                if (symbolTable[iSym].n_value != 0) {
                    uintptr_t symbolBase = symbolTable[iSym].n_value;
                    uintptr_t currentDistance = addressWithSlide - symbolBase;
                    if ((addressWithSlide >= symbolBase) && (currentDistance <= bestDistace)) {
                        bestMatch = symbolTable + iSym;
                        bestDistace = currentDistance;
                    }
                }
            }
            if (bestMatch != NULL) {
                info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
                info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
                if (*info->dli_sname == '_') {
                    info->dli_sname++;
                }
                //如果所有的符号都被删除,就会发生这种情况。
                if (info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                    info->dli_sname = NULL;
                }
                break;
            }
        }
        cmdPointer += loadCmd->cmdsize;
    }
    return true;
}

四、一些特殊的调用栈

看似,我们的抓取方案和抓栈策略都无懈可击。
但在release环境中,由于编译器帮我们做了优化,有一些特殊的调用栈是抓不到的。

1. 尾调用优化

尾调用优化的本质,是 “栈帧” 的复用。
因此,每次压栈都会复用原来的栈帧。
这时候,我们抓到的堆栈永远只有最下层的栈,而中间的调用栈全都丢失了。

PS:关于尾调用优化,我之前实习的时候写了一篇博客。
可供参考:《iOS objc_msgSend尾调用优化详解》

2. 函数内联

这个也比较好理解,因为内联函数会在编译时期展开。
直接复制代码块,从而节省了调用函数带来的额外时间开支。
并且,有的编译器会自动帮我们把一些逻辑简单的函数优化为内联函数。

因此,被编译器优化成内联函数的函数,我们也是没有办法抓到调用栈的。


补:关于如何检测App卡顿?

可参考我之前写的博客:《iOS 性能监控(二)—— 主线程卡顿监控》

我们能感知到的App卡顿,是由于主线程出现卡顿,造成UI更新不及时,从而发生丢帧等情况。(正常情况下,iPhone的屏幕都是60fps,即一秒刷新60次。)

那么,目前比较好的监控方案就是利用runloop原理去监控App状态,

方案如下:

  • 第一步:开启一个子线程,并打开子线程的runloop,让该子线程常驻在App中。

  • 第二步:创建一个RunloopObserverRunloop观察者),将RunloopObserver添加到主线程runloopcommonModes下观察。同时,子线程的runloop开始监听。

  • 第三步:每当主线程runloop的状态发生变化时,就会通知该RunloopObserver。并通过发GCD信号量保证同步操作。同时,子线程的runloop持续监听。

  • 第四步:当主线程的runloop的状态长时间卡在BeforeSourcesAfterWaiting时,就代表当前主线程卡顿。

  • 第五步:检测到卡顿,抓栈,保留现场。 同时,将调用栈信息保存在本地,在合适的时机上报服务端。

正常情况

卡顿情况

Q1:为什么是主线程的 CommonModes
主线程的runloop有DefaultModeUITrackingModeUIInitializationModeGSEventReceiveModeCommonModes
其中,CommonModesDefaultModeUITrackingMode的集合。
正常情况,也是在这两个mode下切换。

Q2:为什么是BeforeSourcesAfterWaiting这两个状态?
这就要说到runloop的执行顺序,
BeforeSources之后,主要是处理Source0事件(响应UIEvent)。如果卡在这个状态过久,说明当前App无法响应点击事件。
AfterWaiting之后,说明当前线程刚从休眠中唤醒,准备执行timer事件。但又卡在这个状态,没有去执行。也能说明当前App卡顿。

PS:更详细监控方案过程,可查看我之前写的博客。
可供参考:《iOS 性能监控(二)—— 主线程卡顿监控》

源码:

GitHub地址:QiStackFrameLogger


参考与致谢:
1.《获取任意线程调用栈的那些事》—— bestswifter
2.《iOS开发高手课》—— 戴铭老师
3.《调用栈》—— 维基百科
4.《Call Stack(调用栈)是什么?》—— 知乎
5.《Virtual Memory(虚拟内存)是什么?》
6.《arm64官方文档》


了解更多iOS及相关新技术,请关注我们的公众号:

image

可添加如下小编微信,并备注加入QiShare技术交流群,小编会邀请你加入《QiShare技术交流群》。

小编微信

关注我们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)

推荐文章:
浅谈编译过程
深入理解HTTPS 浅谈 GPU 及 “App渲染流程”
iOS 查看及导出项目运行日志
Flutter Platform Channel 使用与源码分析
开发没切图怎么办?矢量图标(iconFont)上手指南
DarkMode、WKWebView、苹果登录是否必须适配?
奇舞团安卓团队——aTaller
奇舞周刊