Crash监控平台Sentry的iOS SDK源码解析(二)

2,271

回顾

上篇文章Crash监控平台Sentry的iOS SDK源码解析(一)我们已经了解到Sentry是如何捕获各种异常事件的,其中最重要的一张图如下

那么这篇文章我们就要去细读异常事件是如何被处理和上报的。

源码解析

异常事件归一

其中CrashMonitors分类下的Monitor捕获到的异常最后都会归于一个函数那就是SentryCrashMonitorsentrycrashcm_handleException函数,代码解读如下

void sentrycrashcm_handleException(struct SentryCrash_MonitorContext* context)
{
    context->requiresAsyncSafety = g_requiresAsyncSafety;
    //如果在处理异常的过程中发生了第二个异常
    if(g_crashedDuringExceptionHandling)
    {
        context->crashedDuringCrashHandling = true;
    }
    //遍历所有Monitors,如果Monitor是打开的,就给异常事件添加上下文环境
    for(int i = 0; i < g_monitorsCount; i++)
    {
        Monitor* monitor = &g_monitors[i];
        if(isMonitorEnabled(monitor))
        {
            addContextualInfoToEvent(monitor, context);
        }
    }

    //处理异常事件
    g_onExceptionEvent(context);

    if (context->currentSnapshotUserReported) {
        g_handlingFatalException = false;
    } else {
        //如果不是用户自主上报的异常将关闭所有Monitor
        if(g_handlingFatalException && !g_crashedDuringExceptionHandling) {
            SentryCrashLOG_DEBUG("Exception is fatal. Restoring original handlers.");
            sentrycrashcm_setActiveMonitors(SentryCrashMonitorTypeNone);
        }
    }
}

其中添加事件上下文的函数addContextualInfoToEvent主要用于记录异常现场数据,例如SentryCrashMonitor_AppState记录APP状态信息的代码

static void addContextualInfoToEvent(SentryCrash_MonitorContext* eventContext)
{
    if(g_isEnabled)
    {
        eventContext->AppState.activeDurationSinceLastCrash = g_state.activeDurationSinceLastCrash;
        eventContext->AppState.activeDurationSinceLaunch = g_state.activeDurationSinceLaunch;
        eventContext->AppState.applicationIsActive = g_state.applicationIsActive;
        eventContext->AppState.applicationIsInForeground = g_state.applicationIsInForeground;
        eventContext->AppState.appStateTransitionTime = g_state.appStateTransitionTime;
        eventContext->AppState.backgroundDurationSinceLastCrash = g_state.backgroundDurationSinceLastCrash;
        eventContext->AppState.backgroundDurationSinceLaunch = g_state.backgroundDurationSinceLaunch;
        eventContext->AppState.crashedLastLaunch = g_state.crashedLastLaunch;
        eventContext->AppState.crashedThisLaunch = g_state.crashedThisLaunch;
        eventContext->AppState.launchesSinceLastCrash = g_state.launchesSinceLastCrash;
        eventContext->AppState.sessionsSinceLastCrash = g_state.sessionsSinceLastCrash;
        eventContext->AppState.sessionsSinceLaunch = g_state.sessionsSinceLaunch;
    }
}

其中g_onExceptionEvent是一个函数指针,用于处理异常事件的。该函数指针在初始化的时候已经赋值,具体如下

SentryCrashMonitorType sentrycrash_install(const char* appName, const char* const installPath)
{
    ...
    //设置各种数据的缓存地址
    char path[SentryCrashFU_MAX_PATH_LENGTH];
    snprintf(path, sizeof(path), "%s/Reports", installPath);
    sentrycrashfu_makePath(path);
    sentrycrashcrs_initialize(appName, path);

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

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

    //设置日志发送的时间周期为60秒
    sentrycrashccd_init(60);

    //设置异常事件处理回调函数
    sentrycrashcm_setEventCallback(onCrash);
    //打开各种异常捕获Monitors
    SentryCrashMonitorType monitors = sentrycrash_setMonitoring(g_monitoring);

    SentryCrashLOG_DEBUG("Installation complete.");
    return monitors;
}

事件持久化

我们来看看上文设置的事件处理回调函数onCrash

static void onCrash(struct SentryCrash_MonitorContext* monitorContext)
{
    //如果不是用户主动上报异常,更新并记录APP crash状态
    if (monitorContext->currentSnapshotUserReported == false) {
        SentryCrashLOG_DEBUG("Updating application state to note crash.");
        sentrycrashstate_notifyAppCrash();
    }
    monitorContext->consoleLogPath = g_shouldAddConsoleLogToReport ? g_consoleLogPath : NULL;

    if(monitorContext->crashedDuringCrashHandling)
    {
        //如果在处理异常事件的过程中出现了二次异常,就记录最后的一次异常
        sentrycrashreport_writeRecrashReport(monitorContext, g_lastCrashReportFilePath);
    }
    else
    {
        char crashReportFilePath[SentryCrashFU_MAX_PATH_LENGTH];
        //获取异常事件记录的日志路径
        sentrycrashcrs_getNextCrashReportPath(crashReportFilePath);
        strncpy(g_lastCrashReportFilePath, crashReportFilePath, sizeof(g_lastCrashReportFilePath));
        //将异常事件写入文件
        sentrycrashreport_writeStandardReport(monitorContext, crashReportFilePath);
    }
}

事件发送

在APP Crash的时候记录异常,那什么时机去发送异常日志呢?细心的同学可能在上一篇文章的时候已经发现了,就是在APP下一次启动Sentry初始化的时候啦!在Sentry初始化的地方我们可以看到

- (BOOL)startCrashHandlerWithError:(NSError *_Nullable *_Nullable)error {
    [SentryLog logWithMessage:@"SentryCrashHandler started" andLevel:kSentryLogLevelDebug];
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
        installation = [[SentryInstallation alloc] init];
        [installation install];
        //发送所有缓存的日志
        [installation sendAllReports];
    });
    return YES;
}

主要调用的函数时序图如下

发送的核心代码如下

- (void) sendAllReportsWithCompletion:(SentryCrashReportFilterCompletion) onCompletion
{
    //获取所有异常日志
    NSArray* reports = [self allReports];

    SentryCrashLOG_INFO(@"Sending %d crash reports", [reports count]);

    //发送所有日志
    [self sendReports:reports
         onCompletion:^(NSArray* filteredReports, BOOL completed, NSError* error)
     {
         SentryCrashLOG_DEBUG(@"Process finished with completion: %d", completed);
         if(error != nil)
         {
             SentryCrashLOG_ERROR(@"Failed to send reports: %@", error);
         }
         if((self.deleteBehaviorAfterSendAll == SentryCrashCDeleteOnSucess && completed) ||
            self.deleteBehaviorAfterSendAll == SentryCrashCDeleteAlways)
         {
             //发送成功后删除所有日志
             sentrycrash_deleteAllReports();
         }
         //执行回调
         sentrycrash_callCompletion(onCompletion, filteredReports, completed, error);
     }];
}

总结

总的看下来,Sentry的Crash日志采集的逻辑还是比较简单的,就是拦截Crash事件,记录并上报。看上去很简单,但是实际门槛还是比较高的,比如涉及到内核的方法都不是很常见,所以还是需要很多时间去消化。