iOS 如何抓取線程的“方法調(diào)用?!??

場景:
在一些 “性能監(jiān)控” 的工具中,在檢測到App主線程卡頓的時候,可以通過子線程抓取當(dāng)前時刻所有線程的方法調(diào)用堆棧(保存卡頓現(xiàn)場),并在合適的時機(jī)(WiFi環(huán)境&網(wǎng)絡(luò)環(huán)境較好的時候)把堆棧信息上傳到我們的服務(wù)端。服務(wù)端將堆棧信息過濾分析后,交給客戶端做優(yōu)化處理。
這樣,就能較好的提高用戶的體驗,并及時發(fā)現(xiàn)線上環(huán)境下的問題。
同時,也可以及時發(fā)現(xiàn)問題,及時優(yōu)化我們的代碼質(zhì)量和執(zhí)行效率。
(一個比較好的開發(fā)循環(huán))


call stack

那么,在App發(fā)生卡頓時候,我們該如何抓取方法調(diào)用棧呢?堆棧信息又是什么樣的呢?
本文將通過一個具體的 demo ,闡述如何進(jìn)行抓棧操作。

在此之前,首先要感謝我偶像bestswifter的博客:《獲取任意線程調(diào)用棧的那些事》,對我有很大的啟發(fā)與幫助。

接下來,進(jìn)入我們今天的正題:

  1. 什么是調(diào)用棧?
  2. 如何抓取線程當(dāng)前的調(diào)用棧?
  3. 如何符號化解析?
  4. 一些特殊的調(diào)用棧
  5. (補(bǔ)充)如何檢測App卡頓?

一、什么是調(diào)用棧?

調(diào)用棧(call stack):
是計算機(jī)科學(xué)中存儲有關(guān)正在運(yùn)行的子程序的消息的棧。—— 維基百科

在我們程序運(yùn)行中,通常存在一個函數(shù)調(diào)用另一個函數(shù)的情況。
例如,在某個線程中,調(diào)用了 func A。在 func A 執(zhí)行過程中,調(diào)用了 func B。

那么,在計算機(jī)程序底層需要做哪些事呢?

  1. 轉(zhuǎn)移控制 :暫停 func A ,并開始執(zhí)行 func B,并在 func B執(zhí)行完后,再回到 func A 繼續(xù)執(zhí)行。
  2. 轉(zhuǎn)移數(shù)據(jù)func A 要能把參數(shù)傳遞給 func B,并且 func B如果有返回值的話,要把返回值還給 func A。
  3. 分配和釋放內(nèi)存 :在 func B 開始執(zhí)行時,給需要用到局部變量分配內(nèi)存。在 func B 執(zhí)行完后,釋放這部分內(nèi)存。

舉個例子,
我聲明了兩個函數(shù):foobar
同時,在函數(shù)foo中調(diào)用了函數(shù)bar

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

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

在模擬器(x86)下,會轉(zhuǎn)換成如下匯編:

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

在我的真機(jī)(arm64)下,會轉(zhuǎn)換成如下匯編:

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

再轉(zhuǎn)換成更直觀的圖解,就變成了這樣:

image

目前,絕大部分iOS設(shè)備都是基于arm64架構(gòu)的(iPhone5s及之后發(fā)布的所有設(shè)備)。
通過查詢 arm的官方文檔,我們可以得知:

地址 名稱 作用
sp 棧指針(stack pointer) 存放當(dāng)前函數(shù)的地址。
x30 鏈接寄存器(link register) 存儲函數(shù)的返回地址。
x29 幀指針寄存器(frame pointer) 上一級函數(shù)的地址(與x30一致)。
x19~x28 Callee-saved registers 被調(diào)用這保存寄存器。
x18 The Platform Register 平臺保留,操作系統(tǒng)自身使用。
x17、x16 Intra-procedure-call temporary registers 臨時寄存器。
x9~x15 Temporary registers 臨時寄存器,用來保存本地變量。
x8 Indirect result location register 間接返回地址,返回地址過大時使用。
x0~x7 Parameter/result registers 參數(shù)/返回值寄存器。

其中,比較重要的是棧指針(stack pointer,下面簡稱sp)與幀指針(frame pointer,下面簡稱fp)。
sp會存儲當(dāng)前函數(shù)的棧頂?shù)刂罚?code>fp會存儲上一級函數(shù)的sp


二、如何抓取線程當(dāng)前的調(diào)用棧?

剛才,我們已經(jīng)知道了通過fp就能找到上一級函數(shù)的地址。
通過不停的找上一級fp就能找到當(dāng)前所有方法調(diào)用棧的地址。(回溯法)

Talk is easy, show me code.

  • 第一步:
    首先,我們聲明一個結(jié)構(gòu)體,用來存儲鏈?zhǔn)降臈V羔樞畔?。?code>sp+fp
// 棧幀結(jié)構(gòu)體:
typedef struct QiStackFrameEntry {
    const struct QiStackFrameEntry *const previouts; //!< 上一個棧幀
    const uintptr_t return_address;                  //!< 當(dāng)前棧幀的地址
} 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];
}

具體實現(xiàn):

/*!
 @brief 將machineContext從thread中提取出來
 @param thread 當(dāng)前線程
 @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數(shù)組中。
    直到找到最底層,沒有上一級地址就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進(jìn)行賦值
for (; i<50; i++) {
    backtraceBuffer[i] = frame.return_address; // 把當(dāng)前的地址保存
    if (backtraceBuffer[i] == 0 || frame.previouts == 0 || qi_mach_copyMem(frame.previouts, &frame, sizeof(frame)) != KERN_SUCCESS) {
        break; // 找到原始幀,就break
    }
}

這樣,backtraceBuffer這個數(shù)組中,就存了當(dāng)前時刻線程的方法調(diào)用地址(fp的集合)

backtraceBuffer這個數(shù)組,目前只是一堆方法的地址。
我們并不知道它具體指的是哪個方法?

那就需要接下來的 “符號化解析” 操作。
將每個地址與對應(yīng)符號名(函數(shù)/方法名)一一對應(yīng)上。


三、如何符號化解析?

我們通過回溯幀指針(fp),就能拿到線程下的所有函數(shù)調(diào)用地址。
我們怎么把地址與對應(yīng)的符號(函數(shù)/方法名)對應(yīng)上呢?

這就需要符號化解析步驟。
符號化解析:“地址” => “符號”。

  • 預(yù)備:
    這次不用我們自己聲明了,系統(tǒng)幫我們準(zhǔn)備好了結(jié)構(gòu)體dl_info。
    專門用來存儲當(dāng)前的符號信息。
/*
 * 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;
  • 第一步:
    根據(jù)backtraceBuffer數(shù)組的大小,聲明一個同樣大小的dl_info[]數(shù)組來存符號信息。
int backtraceLength = i;
Dl_info symbolicated[backtraceLength];
qi_symbolicate(backtraceBuffer, symbolicated, backtraceLength, 0); //!< 符號化
  • 第二步:
    通過address找到符號所在的image。
    下面的方法,可以拿到對應(yīng)imageindex(編號)。
// 找出address所對應(yīng)的image編號
uint32_t qi_getImageIndexContainingAddress(const uintptr_t address) {
    const uint32_t imageCount = _dyld_image_count(); // dyld中image的個數(shù)
    const struct mach_header *header = 0;
    
    for (uint32_t i = 0; i < imageCount; i++) {
        header = _dyld_get_image_header(i);
        if (header != NULL) {
            // 在提供的address范圍內(nèi),尋找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所對應(yīng)的imageindex
    我們就可以通過一些系統(tǒng)方法與計算,得到header、虛擬內(nèi)存地址、ASLR偏移量(安全性考慮,為了防黑客入侵。iOS 5、Android 4后引入)。
    以及,比較關(guān)鍵的segmentBase(通過 baseAddress + ASLR 得到)。
const struct mach_header *header = _dyld_get_image_header(index); // 根據(jù)index找到header
const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(index); //image虛擬內(nèi)存地址
const uintptr_t addressWithSlide = address - imageVMAddrSlide; // ASLR偏移量
const uintptr_t segmentBase = qi_getSegmentBaseAddressOfImageIndex(index) + imageVMAddrSlide; // segmentBase是根據(jù)index + ASLR得到的
if (segmentBase == 0) {
    return false;
}

info->dli_fname = _dyld_get_image_name(index);
info->dli_fbase = (void *)header;
  • 第四步:
    通過查找符號表,找到對應(yīng)的符號,并賦值給dl_info數(shù)組。
// 查找符號表,找到對應(yīng)的符號
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 符號表條目的數(shù)量 /
             uint32_t    stroff;     / string table offset 字符串表偏移 /
             uint32_t    strsize;    / string table size in bytes 字符串表的大小(以字節(jié)為單位) /
         };
         */
        
        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++;
            }
            //如果所有的符號都被刪除,就會發(fā)生這種情況。
            if (info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                info->dli_sname = NULL;
            }
            break;
        }
    }
    cmdPointer += loadCmd->cmdsize;
}
  • 第五步:
    遍歷backtraceBuffer數(shù)組,并把符號信息賦值dl_info數(shù)組。
// 符號化:將backtraceBuffer(地址數(shù)組)轉(zhuǎn)成symbolsBuffer(符號數(shù)組)。
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]); //!< 通過回溯得到的棧幀,找到對應(yīng)的符號名。
    }
}
  • 小結(jié):
    符號化解析,完整代碼如下:
#pragma mark - Symbolicate

// 符號化:將backtraceBuffer(地址數(shù)組)轉(zhuǎn)成symbolsBuffer(符號數(shù)組)。
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]); //!< 通過回溯得到的棧幀,找到對應(yīng)的符號名。
    }
}

// 通過address得到當(dāng)前函數(shù)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); // 根據(jù)地址找到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); // 根據(jù)index找到header
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(index); //image虛擬內(nèi)存地址
    const uintptr_t addressWithSlide = address - imageVMAddrSlide; // ASLR偏移量
    const uintptr_t segmentBase = qi_getSegmentBaseAddressOfImageIndex(index) + imageVMAddrSlide; // segmentBase是根據(jù)index + ASLR得到的
    if (segmentBase == 0) {
        return false;
    }
    
    info->dli_fname = _dyld_get_image_name(index);
    info->dli_fbase = (void *)header;
    
    // 查找符號表,找到對應(yīng)的符號
    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 符號表條目的數(shù)量 /
                 uint32_t    stroff;     / string table offset 字符串表偏移 /
                 uint32_t    strsize;    / string table size in bytes 字符串表的大小(以字節(jié)為單位) /
             };
             */
            
            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++;
                }
                //如果所有的符號都被刪除,就會發(fā)生這種情況。
                if (info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                    info->dli_sname = NULL;
                }
                break;
            }
        }
        cmdPointer += loadCmd->cmdsize;
    }
    return true;
}

四、一些特殊的調(diào)用棧

看似,我們的抓取方案和抓棧策略都無懈可擊。
但在release環(huán)境中,由于編譯器幫我們做了優(yōu)化,有一些特殊的調(diào)用棧是抓不到的。

1. 尾調(diào)用優(yōu)化

尾調(diào)用優(yōu)化的本質(zhì),是 “棧幀” 的復(fù)用。
因此,每次壓棧都會復(fù)用原來的棧幀。
這時候,我們抓到的堆棧永遠(yuǎn)只有最下層的棧,而中間的調(diào)用棧全都丟失了。

PS:關(guān)于尾調(diào)用優(yōu)化,我之前實習(xí)的時候?qū)懥艘黄┛汀?br> 可供參考:《iOS objc_msgSend尾調(diào)用優(yōu)化詳解》

2. 函數(shù)內(nèi)聯(lián)

這個也比較好理解,因為內(nèi)聯(lián)函數(shù)會在編譯時期展開。
直接復(fù)制代碼塊,從而節(jié)省了調(diào)用函數(shù)帶來的額外時間開支。
并且,有的編譯器會自動幫我們把一些邏輯簡單的函數(shù)優(yōu)化為內(nèi)聯(lián)函數(shù)。

因此,被編譯器優(yōu)化成內(nèi)聯(lián)函數(shù)的函數(shù),我們也是沒有辦法抓到調(diào)用棧的。


補(bǔ):關(guān)于如何檢測App卡頓?

可參考我之前寫的博客:《iOS 性能監(jiān)控(二)—— 主線程卡頓監(jiān)控》。

我們能感知到的App卡頓,是由于主線程出現(xiàn)卡頓,造成UI更新不及時,從而發(fā)生丟幀等情況。(正常情況下,iPhone的屏幕都是60fps,即一秒刷新60次。)

那么,目前比較好的監(jiān)控方案就是利用runloop原理去監(jiān)控App狀態(tài),

方案如下:

  • 第一步:開啟一個子線程,并打開子線程的runloop,讓該子線程常駐在App中。

  • 第二步:創(chuàng)建一個RunloopObserverRunloop觀察者),將RunloopObserver添加到主線程runloopcommonModes下觀察。同時,子線程的runloop開始監(jiān)聽。

  • 第三步:每當(dāng)主線程runloop的狀態(tài)發(fā)生變化時,就會通知該RunloopObserver。并通過發(fā)GCD信號量保證同步操作。同時,子線程的runloop持續(xù)監(jiān)聽。

  • 第四步:當(dāng)主線程的runloop的狀態(tài)長時間卡在BeforeSourcesAfterWaiting時,就代表當(dāng)前主線程卡頓。

  • 第五步:檢測到卡頓,抓棧,保留現(xiàn)場。
    同時,將調(diào)用棧信息保存在本地,在合適的時機(jī)上報服務(wù)端。

正常情況
卡頓情況

Q1:為什么是主線程的 CommonModes?
主線程的runloop有DefaultMode、UITrackingModeUIInitializationMode、GSEventReceiveMode、CommonModes。
其中,CommonModesDefaultMode、UITrackingMode的集合。
正常情況,也是在這兩個mode下切換。

Q2:為什么是BeforeSources、AfterWaiting這兩個狀態(tài)?
這就要說到runloop的執(zhí)行順序,
BeforeSources之后,主要是處理Source0事件(響應(yīng)UIEvent)。如果卡在這個狀態(tài)過久,說明當(dāng)前App無法響應(yīng)點(diǎn)擊事件。
AfterWaiting之后,說明當(dāng)前線程剛從休眠中喚醒,準(zhǔn)備執(zhí)行timer事件。但又卡在這個狀態(tài),沒有去執(zhí)行。也能說明當(dāng)前App卡頓。

PS:更詳細(xì)監(jiān)控方案過程,可查看我之前寫的博客。
可供參考:《iOS 性能監(jiān)控(二)—— 主線程卡頓監(jiān)控》。

源碼:

GitHub地址:QiStackFrameLogger


參考與致謝:
1.《獲取任意線程調(diào)用棧的那些事》—— bestswifter
2.《iOS開發(fā)高手課》—— 戴銘老師
3.《調(diào)用?!贰?維基百科
4.《Call Stack(調(diào)用棧)是什么?》—— 知乎
5.《Virtual Memory(虛擬內(nèi)存)是什么?》
6.《arm64官方文檔》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容