iOS 如何抓取線程的「方法調用棧」?

級別:★★☆☆☆
標籤:「方法調用棧」「抓取線程的「方法調用棧」」
做者: 647
審校: QiShare團隊php


場景:
在一些 「性能監控」 的工具中,在檢測到App主線程卡頓的時候,能夠經過子線程抓取當前時刻全部線程的方法調用堆棧(保存卡頓現場),並在合適的時機(WiFi環境&網絡環境較好的時候)把堆棧信息上傳到咱們的服務端。服務端將堆棧信息過濾分析後,交給客戶端作優化處理。 這樣,就能較好的提升用戶的體驗,並及時發現線上環境下的問題。
同時,也能夠及時發現問題,及時優化咱們的代碼質量和執行效率。
(一個比較好的開發循環)
git


call stack

那麼,在App發生卡頓時候,咱們該如何抓取方法調用棧呢?堆棧信息又是什麼樣的呢?
本文將經過一個具體的 demo ,闡述如何進行抓棧操做。github

在此以前,首先要感謝我偶像bestswifter的博客:《獲取任意線程調用棧的那些事》,對我有很大的啓發與幫助。swift

接下來,進入咱們今天的正題:數組

  1. 什麼是調用棧?
  2. 如何抓取線程當前的調用棧?
  3. 如何符號化解析?
  4. 一些特殊的調用棧
  5. (補充)如何檢測App卡頓?

1、什麼是調用棧?

調用棧(call stack):
是計算機科學中存儲有關正在運行的子程序的消息的棧。—— 維基百科
安全

在咱們程序運行中,一般存在一個函數調用另外一個函數的狀況。
例如,在某個線程中,調用了 func A。在 func A 執行過程當中,調用了 func Bbash

那麼,在計算機程序底層須要作哪些事呢?微信

  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 平臺保留,操做系統自身使用。
x1七、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


2、如何抓取線程當前的調用棧?

剛纔,咱們已經知道了經過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這個數組,目前只是一堆方法的地址。
咱們並不知道它具體指的是哪一個方法?

那就須要接下來的 「符號化解析」 操做。
將每一個地址與對應符號名(函數/方法名)一一對應上。


3、如何符號化解析?

咱們經過回溯幀指針(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;
}
複製代碼

4、一些特殊的調用棧

看似,咱們的抓取方案和抓棧策略都無懈可擊。
但在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(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公衆號)

推薦文章:
淺談編譯過程
深刻理解HTTPS 淺談 GPU 及 「App渲染流程」
iOS 查看及導出項目運行日誌
Flutter Platform Channel 使用與源碼分析
開發沒切圖怎麼辦?矢量圖標(iconFont)上手指南
DarkMode、WKWebView、蘋果登陸是否必須適配?
奇舞團安卓團隊——aTaller
奇舞週刊

相關文章
相關標籤/搜索