在業(yè)界,用很多有名的 Crash 監(jiān)聽工具,如閉源的 Firebase(Crashlytics)、Bugly 等,也有開源 PLCrashReporter、KSCrash 等。KSCrash 就是其中享有盛名的一個開源庫。KSCrash 有對準(zhǔn)抓取準(zhǔn)確、接口設(shè)計(jì)優(yōu)秀、Crash 文件易于二次分析的優(yōu)點(diǎn)。也因此網(wǎng)上有很多分析 KSCrash 的文章。但這些文章往往太過于追求邏輯細(xì)節(jié),而沒有對整體架構(gòu)的梳理,使得讀者還是難以自己真正理解 KSCrash 。
為了讓大家對 KSCrash 有個整體的理解,我將會使用 UML 圖來分析 KSCrash,不熟悉的讀者建議先自行了解。
本文是Crash 系列的第三篇,這個系列的目錄如下:
- Crash 的監(jiān)聽
- 堆棧分析
- KSCrash 源碼解析
一 KSCrash 的主要模塊
1. KSCrash 的核心
KSCrash 的核心在于KSCrashC.c,這個文件包括了KSCrash 系統(tǒng)的所有重要入口。KSCrash.m是對KSCrashC.c的封裝。
KSCrashC.c的主要部分如下:
-
Installation
kscrash_install()負(fù)責(zé)初始化 KSCrash 系統(tǒng)以監(jiān)聽 Crash。你可以通過kscrash_setMonitoring文件里面的函數(shù)來配置 KSCrash -
Configuration
所有的主要設(shè)置項(xiàng)都要可以通過類似
kscrash_setXYZ的方法設(shè)置 -
App State
KSCrashhook 了大量的app 狀態(tài)變更的通知,這些 hook 方法的命名為kscrash_notifyXYZ。 -
Crash Entry Point
onCrash函數(shù)是處理 Crash 的主要方法,它負(fù)責(zé)獲取 app 狀態(tài),寫 crash 文件(JSON),分析 crash 等。 -
Report Management
這個文件包括一些底層的 C 方法用來處理 crash 文件,如:
kscrash_getReportCount()、kscrash_getReportIDs()、kscrash_readReport()、kscrash_deleteReportWithID()等。 -
Enabling / Disabling KSCrash
你可以用
kscrash_setMonitoring方法設(shè)置啟用某些 crash 監(jiān)聽,如:Mach 監(jiān)聽、Signal 監(jiān)聽等
2. KSCrash 的主要架構(gòu)
下圖是我整理的 KSCrash 核心邏輯。通過下圖可以對KSCrash 有個整體的認(rèn)知:

限于篇幅,上圖只整理了核心的 Crash 監(jiān)聽相關(guān)的邏輯。在上圖我們會看到類似繼承/抽象類的概念。可能有讀者會疑惑,KSCrash 是用 C 語言寫的,哪來的類哪來的繼承呢。
實(shí)際上, 當(dāng)我們細(xì)細(xì)去理解 KSCrash 的代碼,我們會發(fā)現(xiàn)每一個 C 語言的文件的行為其實(shí)類似于一個類,并且每個方法可以理解為類的方法。只是在 C 語言的語法下,很多方法都會有一個參數(shù)表示類實(shí)例。
當(dāng)然一下子看一個整體的類圖,我們也會比較茫然。所以我將會在下面對每個主功能模塊做講解。
二 KSCrash 的主要功能模塊和運(yùn)行邏輯
1. Installation
首先要講的是 KSCrash 的初始化部分,我們可以從這里粗略了解到 KSCrash 是如何設(shè)計(jì)分離各種 Crash 監(jiān)聽模型。

KSCrashMonitorType
這個枚舉定義了所有的 Crash 監(jiān)聽類型,比如 Mach 監(jiān)聽、Signal 監(jiān)聽等。這個枚舉是 Options 類型的,也就是它的值是按位偏移的。這也意味著一個 Type 的值實(shí)際代表的是一個類型集合,比如 KSCrashMonitorTypeAll 代表的是所有監(jiān)聽類型,KSCrashMonitorTypeDebuggerUnsafe代表的是 Xcode 聯(lián)調(diào)的時(shí)候不宜打開的監(jiān)聽類型等。
KSCrashMonitor 和 Monitor
Monitor 是對各種監(jiān)聽的一種抽象,KSCrashMonitor 持有一個 Monitor 類型的數(shù)組,并管理 Type 和 Monitor 的對應(yīng)關(guān)系。在初始化的時(shí)候,KSCrashMonitor 會根據(jù)運(yùn)行環(huán)境,以及傳入?yún)?shù),來決定需要初始化哪些 Crash 監(jiān)聽。并且會循環(huán)執(zhí)行各種監(jiān)聽的初始化工作: setEnabled初始化監(jiān)聽,isEnabled 初始化是否成功。
Monitor
下圖是Monitor抽象結(jié)構(gòu)和各種 KSCrashMonitor的關(guān)系。其中setEnabled、isEnabled、addContextualInfoToEvent是抽象接口,也就是說各種底下的KSCrashMonitor都有這些方法。

當(dāng)然實(shí)際上這種抽象的實(shí)現(xiàn)是依賴于結(jié)構(gòu)體Monitor,他的結(jié)構(gòu)如下:
typedef struct
{
KSCrashMonitorType monitorType;
KSCrashMonitorAPI* (*getAPI)(void);
} Monitor;
typedef struct
{
void (*setEnabled)(bool isEnabled);
bool (*isEnabled)(void);
void (*addContextualInfoToEvent)(struct KSCrash_MonitorContext* eventContext);
} KSCrashMonitorAPI;
在各種Monitor中,會有類似的getAPI方法,其實(shí)就是返回抽象接口對應(yīng)的具體實(shí)現(xiàn)的函數(shù)指針。比如在KSCrashMonitor_MachException中:
KSCrashMonitorAPI* kscm_machexception_getAPI()
{
static KSCrashMonitorAPI api =
{
#if KSCRASH_HAS_MACH
.setEnabled = setEnabled,
.isEnabled = isEnabled,
.addContextualInfoToEvent = addContextualInfoToEvent
#endif
};
return &api;
}
各種監(jiān)聽的初始化流程

KSCrash 的初始化監(jiān)聽的大致調(diào)用流程如上圖所示,需要注意的是setMonitorEnabled后面部分是一個循環(huán)過程,KSCrashMointor_XXX代表各種類型的 Monitor。
2. Crash 信息的存儲結(jié)構(gòu)
在KSCrash中主要依靠KSCrash_MonitorContext、KSMachineContext來記錄 Crash 發(fā)生時(shí)的上下文信息,比如線程信息、寄存器信息、Crash 類型以及特征等。這塊的結(jié)構(gòu)如下:

KSCrash_MonitorContext
記錄 Crash 中的 crash 類型、crash 原因等,詳見 KSCrash 的源碼或者上圖
KSMachineContext
記錄 Crash 發(fā)生在哪個線程,以及 Crash 時(shí)的所有線程列表還有寄存器信息(machineContext)。如上圖所示,KSMachineContext有兩個初始化方法。分別靠Thread和Signal來初始化。
KSStackCursor
可以理解為是一個游標(biāo),指向當(dāng)前正在分析的函數(shù)堆棧位置。這里有個核心的方法advanceCursor,這個方法的作用是函數(shù)堆棧向下溯源。從前面兩篇文章,我們知道不同情況下函數(shù)溯源的方式不同,比如 Mach 異常和 Signal 異??康氖?FP/LR 寄存器信息,NSThread 異常則是靠指針數(shù)組溯源。

上圖是列舉的幾種實(shí)現(xiàn)特殊的 Cursor。比如MachineContext用于需要使用 FP/LR 寄存器溯源的情況,Backtrace用于使用堆棧指針數(shù)組的情況。

上圖是簡單的異常處理類溯源方式的一種對應(yīng)關(guān)系。
3. Crash 的處理過程(KSCrashReport)
首先我們看一下 Crash 處理時(shí)相關(guān)的類:

KSCrashReport
從字面意義上看,KSCrashReport是將 Crash 信息變成文件的處理類。但實(shí)際上他還是 Crash 信息的進(jìn)一步組織整理者。KSCrashReport會將收到的 Crash 信息分析出來,并分析函數(shù)調(diào)用棧、線程等相關(guān)信息,然后將其整理成一個 json 文件。在后續(xù),開發(fā)者可以根據(jù)自己的需要將這個 json 文件變成標(biāo)準(zhǔn)的蘋果 crash 文件或者其他格式的文件
Crash 的處理流程
從抽象角度上講,一個 Crash 的處理流程實(shí)際上就是:
- Monitor 監(jiān)聽到 Crash 發(fā)生
- Mointor 分析 Crash 堆棧信息,錯誤原因等,并生成一個
KSCrash_MonitorContext實(shí)例 - Monitor 調(diào)用
KSCrashMonitor的handleException方法,將第2步生成的 context 實(shí)例傳入。 -
handleException調(diào)用KSCrash的onCrash方法處理 crash -
onCrash調(diào)用KSCrashReport將 Crash 信息轉(zhuǎn)換為標(biāo)準(zhǔn)文件(wrtieStandReport),期間會循環(huán)訪問 Cursor 的advanceCursor方法
以下以 MachException 為例,展示一個 crash 分析堆棧的處理過程。

需要注意的是,
writeAllThreads方法里面有一個特殊的處理邏輯,在循環(huán)處理所有線程時(shí),它會判斷正在處理的線程是不是 Crash 發(fā)生時(shí)的線程,如果是則writeThread將會使用KSCrash_MonitorContext的offendingMachineContext來處理線程的回溯。如果是看過我前兩篇分析 crash 的文章,就會知道 Crash 線程的回溯在不同的情況下各有不同,不能和其他線程采用同樣的處理方式。
三 總結(jié)
到這里,這篇文章就結(jié)束。我沒有像其他分析 KSCrash 的文章一樣去詳細(xì)分析 Mach 異常、Signal 異常等是怎么處理的,因?yàn)槿绾畏治鲞@些異常在前兩篇文章我早已講過。在這篇文章我采用大量的類圖來講解 KSCrash 的結(jié)構(gòu),是希望幫助像我一樣缺少 C 語言開發(fā)經(jīng)驗(yàn)的人可以輕松理解 KSCrash 的架構(gòu),減少理解 KSCrash 的成本。如果有讀者在看完我三篇文章后,還是不能理解 KSCrash 是如何分析 Crash 的,可以結(jié)合本文參考一下網(wǎng)上其他分析 KSCrash 的文章,如iOS Crash 收集框架 KSCrash 源碼解析等。
本文寫于2022牛年的最后一天,提前祝大家新年快樂!如果我的文章幫助到你,請輕輕的點(diǎn)個贊喔~