場景:
在一些 “性能監(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))
那么,在App發(fā)生卡頓時候,我們該如何抓取方法調(diào)用棧呢?堆棧信息又是什么樣的呢?
本文將通過一個具體的 demo ,闡述如何進(jìn)行抓棧操作。
在此之前,首先要感謝我偶像bestswifter的博客:《獲取任意線程調(diào)用棧的那些事》,對我有很大的啟發(fā)與幫助。
接下來,進(jìn)入我們今天的正題:
- 什么是調(diào)用棧?
- 如何抓取線程當(dāng)前的調(diào)用棧?
- 如何符號化解析?
- 一些特殊的調(diào)用棧
- (補(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ī)程序底層需要做哪些事呢?
-
轉(zhuǎn)移控制 :暫停
func A,并開始執(zhí)行func B,并在func B執(zhí)行完后,再回到func A繼續(xù)執(zhí)行。 -
轉(zhuǎn)移數(shù)據(jù) :
func A要能把參數(shù)傳遞給func B,并且func B如果有返回值的話,要把返回值還給func A。 -
分配和釋放內(nèi)存 :在
func B開始執(zhí)行時,給需要用到局部變量分配內(nèi)存。在func B執(zhí)行完后,釋放這部分內(nèi)存。
舉個例子,
我聲明了兩個函數(shù):foo、bar。
同時,在函數(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)換成更直觀的圖解,就變成了這樣:
目前,絕大部分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)image的index(編號)。
// 找出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)的image的index。
我們就可以通過一些系統(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)建一個
RunloopObserver(Runloop觀察者),將RunloopObserver添加到主線程runloop的commonModes下觀察。同時,子線程的runloop開始監(jiān)聽。第三步:每當(dāng)主線程
runloop的狀態(tài)發(fā)生變化時,就會通知該RunloopObserver。并通過發(fā)GCD信號量保證同步操作。同時,子線程的runloop持續(xù)監(jiān)聽。第四步:當(dāng)主線程的
runloop的狀態(tài)長時間卡在BeforeSources、AfterWaiting時,就代表當(dāng)前主線程卡頓。第五步:檢測到卡頓,抓棧,保留現(xiàn)場。
同時,將調(diào)用棧信息保存在本地,在合適的時機(jī)上報服務(wù)端。
Q1:為什么是主線程的
CommonModes?
主線程的runloop有DefaultMode、UITrackingMode、UIInitializationMode、GSEventReceiveMode、CommonModes。
其中,CommonModes是DefaultMode、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官方文檔》