Posts KSCrash
Post
Cancel

KSCrash

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
userInfojson格式的自定义数据,会插入到报表中NSDictionarynil
deleteBehaviorAfterSendAll删除报表的策略枚举KSCDeleteAlways
monitoring监控器,指定监控哪种类型的异常枚举KSCrashMonitorTypeProductionSafeMinimal
deadlockWatchdogInterval主线程死锁阈值,超过则认为死锁,并退出程序,需要设置KSCrashMonitorTypeMainThreadDeadlock才生效double0
searchQueueNames用于获取线程所在队列名称,存在引发崩溃的风险BOOLNO
introspectMemory将OC对象/C字符串相邻堆栈的信息进行记录BOOLYES
catchZombies开启僵尸对象检测功能BOOLNO
doNotIntrospectClasses存放OC类名,其中的包含的类在崩溃信息里只会收集类名,用于保护敏感信息NSArraynil
maxReportCount磁盘中存储最大报表数量int5
sink上报数据的实体对象idnil,必须设置才能上报数据
onCrash崩溃时的回调blockNULL
addConsoleLogToReport崩溃报表中添加log日志BOOLNO
printPreviousLog打印日志输出,可通过console.app查看BOOLNO
demangleLanguages指定需要重整符合的语言类型枚举KSCrashDemangleLanguageAll
uncaughtExceptionHandlerhandleException(exception, false)NSUncaughtExceptionHandlerdebugger情况下为nil/否则KSCrash内部设置值
currentSnapshotUserReportedExceptionHandlerhandleException(exception, true);NSUncaughtExceptionHandlerdebugger情况下为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为例,简单的阐述一下其代码流程:

  1. KSCrashInstallationStandard对象调用- sendAllReportsWithCompletion:方法进行数据上报
  2. - sendAllReportsWithCompletion:方法内部获取当前sink对象,即KSCrashInstallationStandard中提供的sink对象.该sink对象的实际类型为KSCrashReportFilterPipeline.
  3. 然后新建一个KSCrashReportFilterPipeline类型的sink对象,并将原有sink对象添加进新sink对象的数组末尾.
  4. KSCrash保存新的sink对象.并由KSCrash对象调用- sendAllReportsWithCompletion:方法.与KSCrashInstallationStandard调用的- sendAllReportsWithCompletion:方法属于不同的层级,前者是KSCrash层的,后者是位于Installation层.
  5. 通过KSCrash保存的sink对象调用协议方法- filterReports:onCompletion:,因为sink对象KSCrashReportFilterPipeline,所以其具体实现在KSCrashReportFilterPipeline.m中.
  6. 在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中捕获的数据进行上抛,由上层进行记录.

注解:

  1. KSCrashInstallationXYZ.h: 一种省略写法,指具有相同前缀KSCrashInstallation的所有类,如:KSCrashInstallationConsole.h 

This post is licensed under CC BY 4.0 by the author.