![](/assets/img/源码阅读/KSCrash/head.jpeg)
KSCrash是一个异常收集的开源框架.是目前仍在维护,并且相对稳定的框架.国内微信团队开源的Matrix框架的异常收集功能也是基于KSCrash进行的二次开发.
KSCrash功能点:
- 支持符号化
- 生成完整的Apple crash报告
- 支持32位和64位模式
- 支持所有的苹果设备,包括Apple Watch
- 处理只在Mach级别的错误,比如堆栈溢出
- 捕获C++异常
- 在用户崩溃处理程序回调中处理崩溃
- 检测僵尸对象
- 在僵尸对象/内存损坏的场景下恢复丢失的NSException信息
- 内省寄存器和堆栈中的对象
- 提取有关异常引用的对象的信息
- 插件化的报告体系易于适应任何API服务
- 存储堆栈内容
- 诊断崩溃原因
- 以JSON格式记录了Apple report之外的大量额外信息
- 支持自定义提供的额外数据
KSCrash处理以下崩溃类型:
- Mach级异常
- Signal异常
- C++异常
- Objective-C异常
- 主线程死锁
- 自定义异常
KSCrash可以上报数据到以下服务:
本文从自上而下的角度阐述KSCrash(v1.15.21)的分层实现及架构设计.
KSCrash分层结构
KSCrash是作为一个分层体系结构实现的,理论上每一层都可以编译时没有相邻层或上面的层。
1
2
3
4
5
6
7
8
9
+-------------------------------------------------------------+
| Installation |
| +----------------------------------------------------+
| | KSCrash |
| +--------------------------------------------------------+
| | Crash Reporting | Crash Recording | Crash Report Store |
+----+-----------------+-----------------+--------------------+
| Filters | Monitors |
+----------------------+-----------------+
Installation
这个顶层为崩溃系统提供了一个干净的接口,这一级别的API主要用于与后端系统进行通信.
主要入口文件: KSCrashInstallation.h, KSCrashInstallationXYZ.h1
其结构为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── Installations
│ ├── KSCrashInstallation+Alert.h
│ ├── KSCrashInstallation+Alert.m
│ ├── KSCrashInstallation+Private.h
│ ├── KSCrashInstallation.h
│ ├── KSCrashInstallation.m
│ ├── KSCrashInstallationConsole.h
│ ├── KSCrashInstallationConsole.m
│ ├── KSCrashInstallationEmail.h
│ ├── KSCrashInstallationEmail.m
│ ├── KSCrashInstallationQuincyHockey.h
│ ├── KSCrashInstallationQuincyHockey.m
│ ├── KSCrashInstallationStandard.h
│ ├── KSCrashInstallationStandard.m
│ ├── KSCrashInstallationVictory.h
│ └── KSCrashInstallationVictory.m
其中KSCrashInstallation为基本类,提供以下接口:
onCrash
: 崩溃时的一个回调C函数;在该函数里,只能使用异步安全的C方法添加额外的数据,不能使用OC的任何方法.install
: 基本的安装崩溃处理函数接口sendAllReportsWithCompletion:
: 发送本地崩溃报表数据至配置的后台addPreFilter:
添加一个过滤器
在onCrash里可以进行添加一些业务信息进入崩溃的JSON报表数据中,但是使用时需要注意:
在onCrash内部只能使用C函数进行数据的额外设置,不可以使用OC的任何方法.原因是在崩溃处理handler挂起除当前处理线程之外的所有线程之后才进行调用onCrash,此时可用线程仅是当前的处理线程,而OC的方法使用会进行方法/类的查找,这类查找操作会涉及到运行时的一些互斥锁.由于此时已经挂起了线程,在onCrash中调用OC方法很可能会造成死锁问题.具体证明请看另一篇博客https://baidu.com
以上接口是纯净的,暴露给用户直接调用的.除了上述接口外,还有另一部分的私有接口位于KSCrashInstallation+Private.h
中:
1
2
3
4
...
// Create a new sink. Subclasses must implement this.
- (id<KSCrashReportFilter>) sink;
...
这部分接口是用于扩展上报数据至服务器这一功能的.通过继承KSCrashInstallation类,并实现抽象方法sink
方法返回一个遵循KSCrashReportFilter
协议的实例对象进行扩展上报数据到不同服务器的功能.
在这一层设计中,KSCrash默认提供了以下几种方式进行输出崩溃报表数据:
- Console: 直接在Xcode终端进行输出
- Email: 可配置邮箱,将报表以邮件的形式进行发送到指定邮箱
- QuincyKit: 通过设置平台相关参数,上报至平台
- Victory: 通过设置平台相关参数,上报至平台
- Standard: 通过设置url进行发送数据
所以,在这一层里,用户可以很容易的根据自身的业务需求进行扩展上报的实例对象,并且不影响其基础功能.具体方式可参照KSCrashInstallationXYZ.m的实现方法.这一层的设计很好的诠释了开放-封闭原则(Open-Closed Principle).
KSCrash
这一层主要负责安装配置崩溃记录系统与上报系统,完全独立于Installation层.
主要入口文件:KSCrash.h
其结构为:
1
2
3
4
5
├── KSCrash
│ ├── KSCrash.h
│ ├── KSCrash.m
│ ├── Recording
│ └── Reporting
该层对外的API接口主要可分为三类:
- 初始化、安装配置
- App的状态信息
- 崩溃数据的操作API
此外,这层还有一个用于记录自定义异常的接口,可用于存储脚本语言的错误,如webview传回来的js错误等;也可以用于处理崩溃后,自行存储崩溃信息.如,用@try catch处理了崩溃后,在catch中进行调用,通过该接口进行存储上报至服务器,方便进行线上App的崩溃的排查.
1
2
3
4
5
6
7
- (void) reportUserException:(NSString*) name
reason:(NSString*) reason
language:(NSString*) language
lineOfCode:(NSString*) lineOfCode
stackTrace:(NSArray*) stackTrace
logAllThreads:(BOOL) logAllThreads
terminateProgram:(BOOL) terminateProgram;
相关配置
API | 作用 | 类型 | 默认值 |
-initWithBasePath: | 崩溃数据存放路径 | NSString | $APP_HOME/Library/Caches/KSCrashReports |
userInfo | json格式的自定义数据,会插入到报表中 | NSDictionary | nil |
deleteBehaviorAfterSendAll | 删除报表的策略 | 枚举 | KSCDeleteAlways |
monitoring | 监控器,指定监控哪种类型的异常 | 枚举 | KSCrashMonitorTypeProductionSafeMinimal |
deadlockWatchdogInterval | 主线程死锁阈值,超过则认为死锁,并退出程序,需要设置KSCrashMonitorTypeMainThreadDeadlock才生效 | double | 0 |
searchQueueNames | 用于获取线程所在队列名称,存在引发崩溃的风险 | BOOL | NO |
introspectMemory | 将OC对象/C字符串相邻堆栈的信息进行记录 | BOOL | YES |
catchZombies | 开启僵尸对象检测功能 | BOOL | NO |
doNotIntrospectClasses | 存放OC类名,其中的包含的类在崩溃信息里只会收集类名,用于保护敏感信息 | NSArray | nil |
maxReportCount | 磁盘中存储最大报表数量 | int | 5 |
sink | 上报数据的实体对象 | id | nil,必须设置才能上报数据 |
onCrash | 崩溃时的回调 | block | NULL |
addConsoleLogToReport | 崩溃报表中添加log日志 | BOOL | NO |
printPreviousLog | 打印日志输出,可通过console.app查看 | BOOL | NO |
demangleLanguages | 指定需要重整符合的语言类型 | 枚举 | KSCrashDemangleLanguageAll |
uncaughtExceptionHandler | handleException(exception, false) | NSUncaughtExceptionHandler | debugger情况下为nil/否则KSCrash内部设置值 |
currentSnapshotUserReportedExceptionHandler | handleException(exception, true); | NSUncaughtExceptionHandler | debugger情况下为nil/否则KSCrash内部设置值 |
相关信息
API | 作用 |
activeDurationSinceLastCrash | 上一次崩溃到当前App处于前台的总时间 |
backgroundDurationSinceLastCrash | 上一次崩溃到当前App处于后台的总时间 |
launchesSinceLastCrash | 上一次崩溃到当前App启动的总次数 |
sessionsSinceLastCrash | 上一次崩溃到当前App存在内容中Session的个数 |
activeDurationSinceLaunch | 从启动到现在的处于前台总时间 |
backgroundDurationSinceLaunch | 从启动到现在的处于后台总时间 |
sessionsSinceLaunch | 从启动到现在的session个数 |
crashedLastLaunch | 上次崩溃是否是启动时产生 |
reportCount | 未被上报的报表个数 |
systemInfo | 系统信息 |
这部分API进行了跨层调用,其实现是直接访问了Monitors层的KSCrashMonitor_AppState和KSCrashMonitor_System中对应的信息.
崩溃数据的操作
API | 作用 |
-reportIDs | 获取本地所有崩溃报表的id |
-reportWithID: | 根据报表id获取报表数据 |
-deleteAllReports | 删除所有的本地数据 |
-deleteReportWithID: | 根据id删除报表数据 |
Crash Reporting
这一层主要是为Installation层提供数据格式的处理与转换和向服务器发送报告的功能.
主要文件: KSCrashReportFilter.h、KSCrashReportFilterBasic.h
其结构为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
├── Reporting
│ ├── Filters
│ │ ├── KSCrashReportFilter.h
│ │ ├── KSCrashReportFilterAlert.h
│ │ ├── KSCrashReportFilterAlert.m
│ │ ├── KSCrashReportFilterAppleFmt.h
│ │ ├── KSCrashReportFilterAppleFmt.m
│ │ ├── KSCrashReportFilterBasic.h
│ │ ├── KSCrashReportFilterBasic.m
│ │ ├── KSCrashReportFilterGZip.h
│ │ ├── KSCrashReportFilterGZip.m
│ │ ├── KSCrashReportFilterJSON.h
│ │ ├── KSCrashReportFilterJSON.m
│ │ ├── KSCrashReportFilterSets.h
│ │ ├── KSCrashReportFilterSets.m
│ │ ├── KSCrashReportFilterStringify.h
│ │ ├── KSCrashReportFilterStringify.m
│ ├── Sinks
│ │ ├── KSCrashReportSinkConsole.h
│ │ ├── KSCrashReportSinkConsole.m
│ │ ├── KSCrashReportSinkEMail.h
│ │ ├── KSCrashReportSinkEMail.m
│ │ ├── KSCrashReportSinkQuincyHockey.h
│ │ ├── KSCrashReportSinkQuincyHockey.m
│ │ ├── KSCrashReportSinkStandard.h
│ │ ├── KSCrashReportSinkStandard.m
│ │ ├── KSCrashReportSinkVictory.h
│ │ └── KSCrashReportSinkVictory.m
│ └── Tools
这一层的抽象接口是文件KSCrashReportFilter.h中,是一个KSCrashReportFilter协议,其代理方法如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* A filter receives a set of reports, possibly transforms them, and then
* calls a completion method.
*/
@protocol KSCrashReportFilter <NSObject>
/** Filter the specified reports.
*
* @param reports The reports to process.
* @param onCompletion Block to call when processing is complete.
*/
- (void) filterReports:(NSArray*) reports
onCompletion:(KSCrashReportFilterCompletion) onCompletion;
@end
该代理方法贯穿整个Crash Reporting层,是该层的核心抽象方法.不管是Sinks还是Filters的实现里都包含了这个代理方法的具体实现逻辑;不同的是,在Sinks中的对应实现是进行网络的请求,进行数据的上报操作,Filters对应的实现是进行数据格式的转换操作.在调用顺序上是由Filters的该代理方法进行数据转换后,再调用Sinks中的该代理方法进行数据上报.
这里面的代码调用流程有点绕,这边以KSCrashInstallationStandard为例,简单的阐述一下其代码流程:
- KSCrashInstallationStandard对象调用
- sendAllReportsWithCompletion:
方法进行数据上报 - 在
- sendAllReportsWithCompletion:
方法内部获取当前sink对象,即KSCrashInstallationStandard中提供的sink对象.该sink对象的实际类型为KSCrashReportFilterPipeline. - 然后新建一个KSCrashReportFilterPipeline类型的sink对象,并将原有sink对象添加进新sink对象的数组末尾.
- KSCrash保存新的sink对象.并由KSCrash对象调用
- sendAllReportsWithCompletion:
方法.与KSCrashInstallationStandard调用的- sendAllReportsWithCompletion:
方法属于不同的层级,前者是KSCrash层的,后者是位于Installation层. - 通过KSCrash保存的sink对象调用协议方法
- filterReports:onCompletion:
,因为sink对象KSCrashReportFilterPipeline,所以其具体实现在KSCrashReportFilterPipeline.m中. - 在KSCrashReportFilterPipeline.m中的协议方法实现中会进行递归调用其内部保存的数组元素,这些元素也是实现了KSCrashReportFilter协议的,所以这里进行了递归调用元素的协议方法.这个例子中只有一个可用元素,即KSCrashReportSinkStandard对象,所以最终调用了KSCrashReportSinkStandard.m里面的协议方法进行网络请求.
在步骤2中获取的sink对象就是KSCrashInstallationXYZ中实现的具体sink对象,该sink对象是KSCrashReportFilterPipeline类型的对象,该对象中filters数组中包含了多种filter对象,且会将KSCrashReportSinkXYZ添加至数组末尾;所以当遍历调用所有的filter对应的协议方法时,由于KSCrashReportSinkXYZ类型的对象位于数组末尾,最终可以通过KSCrashReportSinkXYZ中的代理方法进行数据的上报.
Filters提供数据转换成指定格式的功能,大部分文件名中突出了其能转换的格式.其中解释一下KSCrashReportFilterSets和KSCrashReportFilterBasic的功能.
KSCrashReportFilterBasic: 是一个提供多种聚合filters方式的工具类,举个栗子:KSCrashReportFilterPipeline提供类似管道的方式进行数据的处理,通过依次添加不同类型的filter,如AppleFmt、StringToData、GZipCompress,就可以将报表数组依次进行Apple report格式、字符串转为Data、最终进行zip的压缩的格式转换.
KSCrashReportFilterSets: 创建一个Apple report格式的并包含系统信息和用户自定义信息的数据提取器.可以通过模仿该类实现提取感兴趣的数据.
这一层的设计主要依赖于抽象方法- (void) filterReports:onCompletion:
,很好的诠释了依赖倒置原则
Crash Recording
这一层主要提供记录单个崩溃事件功能,其实现在C层是异步安全的.
主要入口文件为: KSCrashC.h
其结构为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
├── Recording
│ ├── KSCrash.h
│ ├── KSCrash.m
│ ├── KSCrashC.c
│ ├── KSCrashC.h
│ ├── KSCrashCachedData.c
│ ├── KSCrashCachedData.h
│ ├── KSCrashDoctor.h
│ ├── KSCrashDoctor.m
│ ├── KSCrashReport.c
│ ├── KSCrashReport.h
│ ├── KSCrashReportFields.h
│ ├── KSCrashReportFixer.c
│ ├── KSCrashReportFixer.h
│ ├── KSCrashReportStore.c
│ ├── KSCrashReportStore.h
│ ├── KSCrashReportVersion.h
│ ├── KSCrashReportWriter.h
│ ├── KSSystemCapabilities.h
│ ├── Monitors
│ └── Tools
这一层的功能主要有:
- 提供上层对应的接口实现
- Monitors层提供捕获崩溃数据的功能
- KSCrashCachedData记录所有线程信息的实时缓存
- KSCrashDoctor提供上报数据前对崩溃的诊断
- KSCrashReport提供将Monitors层上抛的数据进行json格式本地存储
- KSCrashReportFixer提供在读取本地崩溃数据时进行修复可能不完善的json格式数据的功能
Crash Report Store
这一层主要提供崩溃报表和其他配置数据的存储和检索;还提供文件路径,以便其他层进行更基本的访问.
主要入口文件: KSCrashReportStore.h
其结构为:
1
2
├── Crash Report Store
│ └── KSCrashReportStore.h
KSCrashReportStore提供对本地崩溃数据文件管理的功能,包含存储路径的初始化,报表的读取,删除,数量限制等.
Monitors
这一层主要用于捕获程序错误,并将崩溃数据往上层(Recording层)抛.
这层可以处理以下异常:
- Mach Exception
- Signal
- NSException
- Main Thread Deadlock
- 僵尸对象
- 自定义异常
- C++异常
主要入口文件: KSCrashMonitor.h
其结构为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
├── Monitors
│ │ ├── KSCrashMonitor.c
│ │ ├── KSCrashMonitor.h
│ │ ├── KSCrashMonitorContext.h
│ │ ├── KSCrashMonitorType.c
│ │ ├── KSCrashMonitorType.h
│ │ ├── KSCrashMonitor_AppState.c
│ │ ├── KSCrashMonitor_AppState.h
│ │ ├── KSCrashMonitor_CPPException.cpp
│ │ ├── KSCrashMonitor_CPPException.h
│ │ ├── KSCrashMonitor_Deadlock.h
│ │ ├── KSCrashMonitor_Deadlock.m
│ │ ├── KSCrashMonitor_MachException.c
│ │ ├── KSCrashMonitor_MachException.h
│ │ ├── KSCrashMonitor_NSException.h
│ │ ├── KSCrashMonitor_NSException.m
│ │ ├── KSCrashMonitor_Signal.c
│ │ ├── KSCrashMonitor_Signal.h
│ │ ├── KSCrashMonitor_System.h
│ │ ├── KSCrashMonitor_System.m
│ │ ├── KSCrashMonitor_User.c
│ │ ├── KSCrashMonitor_User.h
│ │ ├── KSCrashMonitor_Zombie.c
│ │ └── KSCrashMonitor_Zombie.h
这一层的抽象API为KSCrashMonitorAPI,是一个结构体位于KSCrashMonitor.h中,其定义如下:
typedef struct
{
void (*setEnabled)(bool isEnabled);
bool (*isEnabled)(void);
void (*addContextualInfoToEvent)(struct KSCrash_MonitorContext* eventContext);
} KSCrashMonitorAPI;
该API是通过Enabled控制monitor实例的安装与卸载
在KSCrashMonitor.c
中维护了一个monitors数组,记录各个monitors的对应实现与类型.在初始化时,通过判断环境进行monitors的安全设置,举个栗子:如果此时处于Xcode调试状态,则会将崩溃捕获相关的API的enable置为NO,禁止调用对应的安装方法.
并且在该文件中将各个monitors中捕获的数据进行上抛,由上层进行记录.
注解:
KSCrashInstallationXYZ.h: 一种省略写法,指具有相同前缀KSCrashInstallation的所有类,如:KSCrashInstallationConsole.h ↩