KSCrash 源码笔记 - 安装篇

KSCrash 是业界知名的 Crash 收集框架,其功能想必不用过多介绍。本系列笔记主要是想搞清楚这个框架是如何使用的、如何捕获到崩溃的、如何收集崩溃的。

使用示例

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- (void) installCrashHandler
{
// Create an installation (choose one)
// KSCrashInstallation* installation = [self makeStandardInstallation];
KSCrashInstallation* installation = [self makeEmailInstallation];
// KSCrashInstallation* installation = [self makeHockeyInstallation];
// KSCrashInstallation* installation = [self makeQuincyInstallation];
// KSCrashInstallation *installation = [self makeVictoryInstallation];


// Install the crash handler. This should be done as early as possible.
// This will record any crashes that occur, but it doesn't automatically send them.
[installation install];
[KSCrash sharedInstance].deleteBehaviorAfterSendAll = KSCDeleteNever; // TODO: Remove this


// Send all outstanding reports. You can do this any time; it doesn't need
// to happen right as the app launches. Advanced-Example shows how to defer
// displaying the main view controller until crash reporting completes.
[installation sendAllReportsWithCompletion:^(NSArray* reports, BOOL completed, NSError* error)
{
if(completed)
{
NSLog(@"Sent %d reports", (int)[reports count]);
}
else
{
NSLog(@"Failed to send reports: %@", error);
}
}];
}

- (KSCrashInstallation*) makeEmailInstallation
{
NSString* emailAddress = @"your@email.here";

KSCrashInstallationEmail* email = [KSCrashInstallationEmail sharedInstance];
email.recipients = @[emailAddress];
email.subject = @"Crash Report";
email.message = @"This is a crash report";
email.filenameFmt = @"crash-report-%d.txt.gz";

[email addConditionalAlertWithTitle:@"Crash Detected"
message:@"The app crashed last time it was launched. Send a crash report?"
yesAnswer:@"Sure!"
noAnswer:@"No thanks"];

// Uncomment to send Apple style reports instead of JSON.
[email setReportStyle:KSCrashEmailReportStyleApple useDefaultFilenameFormat:YES];

return email;
}

这是 KSCrash 官方提供的示例代码,支持通过 url、email 等方法上报日志。

注册篇

从上一整节使用示例里也能看到,KSCrash 的注册关键在于 [installation install]; 这行方法,进行注册后才能进行 Crash 的捕获。该方法的具体内容:

1
2
3
4
5
6
7
8
9
10
- (void) install
{
KSCrash* handler = [KSCrash sharedInstance];
@synchronized(handler)
{
g_crashHandlerData = self.crashHandlerData;
handler.onCrash = crashCallback;
[handler install];
}
}

上述方法又调用了 KSCrash 类的 install 方法,该方法的具体内容:

1
2
3
4
5
6
7
8
9
10
11
12
- (BOOL) install
{
// 调用 kscrash_install 进行注册操作。传入 bundle name 和崩溃日志的存储路径
_monitoring = kscrash_install(self.bundleName.UTF8String,
self.basePath.UTF8String);
if(self.monitoring == 0)
{
return false;
}

return true;
}

上述方法又调用了 kscrash_install 方法,该方法的具体内容:

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
30
31
32
33
34
35
36
37
38
39
40
41
KSCrashMonitorType kscrash_install(const char* appName, const char* const installPath)
{
KSLOG_DEBUG("Installing crash reporter.");

if(g_installed)
{
KSLOG_DEBUG("Crash reporter already installed.");
return g_monitoring;
}
g_installed = 1;

char path[KSFU_MAX_PATH_LENGTH];
snprintf(path, sizeof(path), "%s/Reports", installPath);
ksfu_makePath(path);
kscrs_initialize(appName, path);

snprintf(path, sizeof(path), "%s/Data", installPath);
ksfu_makePath(path);
snprintf(path, sizeof(path), "%s/Data/CrashState.json", installPath);
kscrashstate_initialize(path);

snprintf(g_consoleLogPath, sizeof(g_consoleLogPath), "%s/Data/ConsoleLog.txt", installPath);
if(g_shouldPrintPreviousLog)
{
printPreviousLog(g_consoleLogPath);
}
kslog_setLogFilename(g_consoleLogPath, true);

ksccd_init(60);

// 保存 onCrash 到 g_onExceptionEvent
kscm_setEventCallback(onCrash);
// 添加监控,捕获崩溃时会调用 g_onExceptionEvent 即 onCrash 方法
KSCrashMonitorType monitors = kscrash_setMonitoring(g_monitoring);

KSLOG_DEBUG("Installation complete.");

notifyOfBeforeInstallationState();

return monitors;
}

其中,调用了关键方法 kscm_setEventCallbackkscrash_setMonitoring

kscm_setEventCallback

kscm_setEventCallback 方法用于将的入参为 onCrash 回调方法保存起来,在捕获到 Crash 之后进行统一调用。

onCrash 方法很关键,后期捕获到各种类型的 Crash 后都会调用这个方法。这里先贴下代码,后边在捕获到 Crash 之后的流程里再具体分析。

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
static void onCrash(struct KSCrash_MonitorContext* monitorContext)
{
if (monitorContext->currentSnapshotUserReported == false) {
KSLOG_DEBUG("Updating application state to note crash.");
kscrashstate_notifyAppCrash();
}
monitorContext->consoleLogPath = g_shouldAddConsoleLogToReport ? g_consoleLogPath : NULL;

if(monitorContext->crashedDuringCrashHandling)
{
kscrashreport_writeRecrashReport(monitorContext, g_lastCrashReportFilePath);
}
else
{
char crashReportFilePath[KSFU_MAX_PATH_LENGTH];
int64_t reportID = kscrs_getNextCrashReport(crashReportFilePath);
strncpy(g_lastCrashReportFilePath, crashReportFilePath, sizeof(g_lastCrashReportFilePath));
kscrashreport_writeStandardReport(monitorContext, crashReportFilePath);

if(g_reportWrittenCallback)
{
g_reportWrittenCallback(reportID);
}
}
}

我们来看下 kscm_setEventCallback 方法的具体内容

1
2
3
4
void kscm_setEventCallback(void (*onEvent)(struct KSCrash_MonitorContext* monitorContext))
{
g_onExceptionEvent = onEvent;
}

kscm_setEventCallback 方法只是把传入的 onCrash 方法赋值给了 g_onExceptionEvent 全局变量。尝试在工程中全局搜索 g_onExceptionEvent,会发现这个方法被调用的地方,在 kscm_handleException 这个方法里,该方法的具体内容:

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
void kscm_handleException(struct KSCrash_MonitorContext* context)
{
context->requiresAsyncSafety = g_requiresAsyncSafety;
if(g_crashedDuringExceptionHandling)
{
context->crashedDuringCrashHandling = true;
}
for(int i = 0; i < g_monitorsCount; i++)
{
Monitor* monitor = &g_monitors[i];
if(isMonitorEnabled(monitor))
{
addContextualInfoToEvent(monitor, context);
}
}

// 调用 onCrash 方法
g_onExceptionEvent(context);

if (context->currentSnapshotUserReported) {
g_handlingFatalException = false;
} else {
if(g_handlingFatalException && !g_crashedDuringExceptionHandling) {
KSLOG_DEBUG("Exception is fatal. Restoring original handlers.");
kscm_setActiveMonitors(KSCrashMonitorTypeNone);
}
}
}

尝试在工程中全局搜索下 kscm_handleException,会发现这个方法在各种类型 Crash 被捕获的时候会被调用。

kscrash_setMonitoring

kscrash_setMonitoring 方法用于设置对各种 Crash 类型的监控。

我们来看下 kscrash_setMonitoring 方法的具体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
KSCrashMonitorType kscrash_setMonitoring(KSCrashMonitorType monitors)
{
g_monitoring = monitors;

if(g_installed)
{
// 设置崩溃监控
kscm_setActiveMonitors(monitors);
return kscm_getActiveMonitors();
}
// Return what we will be monitoring in future.
return g_monitoring;
}

上述方法根据 g_installed 标记位判断是否进行崩溃监控。调用了 kscm_setActiveMonitors 方法,该方法的具体内容:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void kscm_setActiveMonitors(KSCrashMonitorType monitorTypes)
{
// 1. 判断是否为 Debug 环境、判断 monitorTypes 是否包含 KSCrashMonitorTypeDebuggerUnsafe 选项
if(ksdebug_isBeingTraced() && (monitorTypes & KSCrashMonitorTypeDebuggerUnsafe))
{
static bool hasWarned = false;
if(!hasWarned)
{
hasWarned = true;
KSLOGBASIC_WARN(" ************************ Crash Handler Notice ************************");
KSLOGBASIC_WARN(" * App is running in a debugger. Masking out unsafe monitors. *");
KSLOGBASIC_WARN(" * This means that most crashes WILL NOT BE RECORDED while debugging! *");
KSLOGBASIC_WARN(" **********************************************************************");
}
// 将 monitorTypes 仅保留 KSCrashMonitorTypeDebuggerSafe 选项
monitorTypes &= KSCrashMonitorTypeDebuggerSafe;
}
// 2. 判断是否要求异步安全、判断 monitorTypes 是否包含 KSCrashMonitorTypeAsyncUnsafe 选项
if(g_requiresAsyncSafety && (monitorTypes & KSCrashMonitorTypeAsyncUnsafe))
{
KSLOG_DEBUG("Async-safe environment detected. Masking out unsafe monitors.");
// 将 monitorTypes 仅保留 KSCrashMonitorTypeAsyncSafe 选项
monitorTypes &= KSCrashMonitorTypeAsyncSafe;
}

KSLOG_DEBUG("Changing active monitors from 0x%x tp 0x%x.", g_activeMonitors, monitorTypes);

KSCrashMonitorType activeMonitors = KSCrashMonitorTypeNone;
// 3. 根据 g_monitorsCount 遍历 g_monitors 数组
for(int i = 0; i < g_monitorsCount; i++)
{
// 3.1. 获取 monitor
Monitor* monitor = &g_monitors[i];
// 3.2. 判断该 monitor 是否要被监控
bool isEnabled = monitor->monitorType & monitorTypes;
// 3.3. 设置该 monitor 是否被监控
setMonitorEnabled(monitor, isEnabled);
if(isMonitorEnabled(monitor))
{
activeMonitors |= monitor->monitorType;
}
else
{
activeMonitors &= ~monitor->monitorType;
}
}

KSLOG_DEBUG("Active monitors are now 0x%x.", activeMonitors);
g_activeMonitors = activeMonitors;
}

KSCrash 做了 Debug 监控,通过 ksdebug_isBeingTraced() 方法判断当前是否为 Debug 环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool ksdebug_isBeingTraced(void)
{
struct kinfo_proc procInfo;
size_t structSize = sizeof(procInfo);
int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};

if(sysctl(mib, sizeof(mib)/sizeof(*mib), &procInfo, &structSize, NULL, 0) != 0)
{
KSLOG_ERROR("sysctl: %s", strerror(errno));
return false;
}

return (procInfo.kp_proc.p_flag & P_TRACED) != 0;
}

再回到 kscm_setActiveMonitors 方法,首先做的是判断当前为 Debug 环境的话,就剔除掉对 MachException、Signal、CPPException、NSException 这几类 Crash 的监控。
具体做法就是根据传入的 monitorTypes 和 KSCrashMonitorTypeDebuggerUnsafe 做与操作判断:
(monitorTypes & KSCrashMonitorTypeDebuggerUnsafe)

结果为 true 的话,将 monitorTypes 只保留 KSCrashMonitorTypeDebuggerSafe 选项:
monitorTypes &= KSCrashMonitorTypeDebuggerSafe;

KSCrashMonitorTypeDebuggerUnsafe 和 KSCrashMonitorTypeDebuggerSafe 的定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define KSCrashMonitorTypeAll              \
( \
KSCrashMonitorTypeMachException | \
KSCrashMonitorTypeSignal | \
KSCrashMonitorTypeCPPException | \
KSCrashMonitorTypeNSException | \
KSCrashMonitorTypeMainThreadDeadlock | \
KSCrashMonitorTypeUserReported | \
KSCrashMonitorTypeSystem | \
KSCrashMonitorTypeApplicationState | \
KSCrashMonitorTypeZombie \
)

#define KSCrashMonitorTypeDebuggerUnsafe \
( \
KSCrashMonitorTypeMachException | \
KSCrashMonitorTypeSignal | \
KSCrashMonitorTypeCPPException | \
KSCrashMonitorTypeNSException \
)

/** Monitors that are safe to enable in a debugger. */
#define KSCrashMonitorTypeDebuggerSafe (KSCrashMonitorTypeAll & (~KSCrashMonitorTypeDebuggerUnsafe))

g_monitorsCount 的数据来自:

1
static int g_monitorsCount = sizeof(g_monitors) / sizeof(*g_monitors);

g_monitors 的数据来自:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static Monitor g_monitors[] =
{
#if KSCRASH_HAS_MACH
{
.monitorType = KSCrashMonitorTypeMachException,
.getAPI = kscm_machexception_getAPI,
},
#endif
#if KSCRASH_HAS_SIGNAL
{
.monitorType = KSCrashMonitorTypeSignal,
.getAPI = kscm_signal_getAPI,
},
#endif
#if KSCRASH_HAS_OBJC
{
.monitorType = KSCrashMonitorTypeNSException,
.getAPI = kscm_nsexception_getAPI,
},
{
.monitorType = KSCrashMonitorTypeMainThreadDeadlock,
.getAPI = kscm_deadlock_getAPI,
},
{
.monitorType = KSCrashMonitorTypeZombie,
.getAPI = kscm_zombie_getAPI,
},
#endif
{
.monitorType = KSCrashMonitorTypeCPPException,
.getAPI = kscm_cppexception_getAPI,
},
{
.monitorType = KSCrashMonitorTypeUserReported,
.getAPI = kscm_user_getAPI,
},
{
.monitorType = KSCrashMonitorTypeSystem,
.getAPI = kscm_system_getAPI,
},
{
.monitorType = KSCrashMonitorTypeApplicationState,
.getAPI = kscm_appstate_getAPI,
},
};

这个 g_monitors 数组包含了 KSCrash 支持的各种崩溃监控。

遍历 g_monitors 数组,对每一项 monitor 进行 setMonitorEnabled 方法的调用:

1
2
3
4
5
6
7
8
static inline void setMonitorEnabled(Monitor* monitor, bool isEnabled)
{
KSCrashMonitorAPI* api = getAPI(monitor);
if(api != NULL && api->setEnabled != NULL)
{
api->setEnabled(isEnabled);
}
}

在各种支持的崩溃类型里,会实现 setEnabled 方法,进行对各自崩溃类型的监控。

小结

到这里,KSCrash 的注册流程就很清楚了,通过注册 install 方法,调用 kscm_setEventCallback 方法将 onCrash 传递给一个全局变量,在各种类型的 Crash 被捕获时会调用这个 onCrash 方法。调用 kscrash_setMonitoring 方法中对各种类型的崩溃进行添加监控。

对于各种类型的崩溃是如何添加监控,以及捕获到崩溃之后是如果处理崩溃的,我们在后续篇章中再展开分析。