异常捕获/收集的平台有很多,我们选用了Sentry;Sentry支持搭建在自己的服务器上(self-hosted),支持多种编程语言,号称是有超过5万家公司的100万名开发人员在使用;Sentry提供了3种类型账号:
Developer
,Team
,Business
;Developer类型是免费的,但功能有限,且异常记录每个月最多5K条(这个数量,笔者还特意测试了下,异常记录到达5K后,记录列表将会提示让你升级到付费版);如果免费版不能满足要求,就需付费使用;有条件的最好是自建服务器,异常记录数是没有限制的;关于self-hosted搭建官方文档有详细的教程,本文我们主要只讲解iOS端集成Sentry的过程;
创建项目
Sentry官网注册账号,创建一个Objective-C或者Swift项目;
然后找到项目设置选择Client Keys(DSN)
,复制DSN后面代码中需要用到(如果是self-hosted则是Public DSN);
代码实现
pod导入Sentry库;
appDelegate的didFinishLaunchingWithOptions方法中启动sentry捕获;
- (void)startSentry {
NSError *error = nil;
// 根据DSN创建SentryClient
SentryClient *client = [[SentryClient alloc] initWithDsn:kSentryDSN didFailWithError:&error];
SentryClient.sharedClient = client;
[SentryClient.sharedClient startCrashHandlerWithError:&error];
if (nil != error) {
NSLog(@"%@", error);
}
}
Sentry提供了一系列属性,供我们自定义一些信息;
SentryClient.sharedClient.environment = environment; // 环境 例如:debug
[SentryClient.sharedClient enableAutomaticBreadcrumbTracking]; // 开启面包屑功能
SentryClient.sharedClient.maxBreadcrumbs = 30; // 面包屑最多栈数
// 用户信息
SentryUser *user = [[SentryUser alloc] initWithUserId:guid]; // 日志记录以此区别、归类不同用户
user.username = userName;
user.extra = @{@"cellphone":cellphone}; // 自定义字段用户信息
SentryClient.sharedClient.user = user;
SentryClient.sharedClient.extra = @{@"other":otherMsg}; // 自定义字段信息
至此就已经实现对异常的监听、捕获了;
源码窥探
sentry比较强大,监听了各种各样情况的Crash异常;从源码中可以大致窥探其支持的Crash异常类型:
这其中包括C++、死锁、僵尸对象等等异常;我们比较熟悉的可能就是NSException
了,它只包括Foundation框架的比如数组越界、数组,字典插入nil对象等情况;接下来我们就看下SentryCrashMonitor_NSException源码实现,(其他类型暂时不管,看着头大);
g_isEnabled = isEnabled;
if(isEnabled)
{
SentryCrashLOG_DEBUG(@"Backing up original handler.");
g_previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
SentryCrashLOG_DEBUG(@"Setting new handler.");
NSSetUncaughtExceptionHandler(&handleUncaughtException);
SentryCrash.sharedInstance.uncaughtExceptionHandler = &handleUncaughtException;
SentryCrash.sharedInstance.currentSnapshotUserReportedExceptionHandler = &handleCurrentSnapshotUserReportedException;
}
如果开启捕获(调试阶段sentry是不开启捕获的),则先使用g_previousUncaughtExceptionHandler
记录之前捕获异常的函数指针;然后通过NSSetUncaughtExceptionHandler设置sentry的异常捕获函数;
异常发生时就会调用函数
static void handleException(NSException* exception, BOOL currentSnapshotUserReported) {
SentryCrashLOG_DEBUG(@"Trapped exception %@", exception);
if(g_isEnabled)
{
....
SentryCrash_MonitorContext* crashContext = &g_monitorContext;
memset(crashContext, 0, sizeof(*crashContext));
crashContext->crashType = SentryCrashMonitorTypeNSException;
crashContext->eventID = eventID;
crashContext->offendingMachineContext = machineContext;
crashContext->registersAreValid = false;
crashContext->NSException.name = [[exception name] UTF8String];
crashContext->NSException.userInfo = [[NSString stringWithFormat:@"%@", exception.userInfo] UTF8String];
...
if (g_previousUncaughtExceptionHandler != NULL)
{
SentryCrashLOG_DEBUG(@"Calling original exception handler.");
g_previousUncaughtExceptionHandler(exception);
}
}
}
主要配置一些异常的信息,然后将信息存储起来;以备下次启动应用时再调用接口上传这些数据;最后再调用之前的捕获异常函数,这里主要的作用就是兼容其他异常捕获功能;因为其他代码也可能调用了NSSetUncaughtExceptionHandler设置捕获函数;
自定义Events
除了捕获异常,sentry还支持发送自定义的日志信息,比如网络请求失败就可以将失败信息上传;
SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentrySeverityWarning]; // 指定事件的严重级别 Fatal/Error/Warnig
event.message = message; // 错误信息
event.environment = environment;
event.extra =@{@"url":url,@"code":@(code),@"param":param}; // 自定义字段
[SentryClient.sharedClient sendEvent:event withCompletionHandler:^(NSError * _Nullable error) {
if (nil != error) {
NSLog(@"%@", error);
}
}];
测试
sentry代码都写好后,我们手动抛个异常测下sentry平台是否能正常统计异常的数据;
一切没问题的话,后台我们就能看到日志记录了:
- 面包屑信息
- 方法调用栈
因为我们还没有上传dSYM符号文件,sentry不能解析crash日志定位到具体方法;
上传dSYM文件
sentry平台创建Token:User Setting ----> Auth Tokens ---> Create New Token;创建时按需勾选选项;
拿到Token后就可以上传文件了
有两种上传方式
- shell脚本上传
- 先打包,然后拿到dSYM文件;
- 安装sentry-cli:
brew install getsentry/tools/sentry-cli - 编写并执行以下脚本即可,其中
URL
如果是sentry服务器则是https://sentry.io
,如果是self-hosted则填写自己服务器url;
sentry-cli --url URL --auth-token YOUR_TOKEN upload-dif --org YOUR_ORG --project YOUR_PROJECT dSYM_PATH --log-level=debug
- 通过fastlane上传
- 安装fastlane
sudo gem install fastlane -NV
或是brew cask install fastlane
命令安装;
安装完后执行命令fastlane --version
,确认安装成功; - 初始化fastlane
cd到项目目录下,执行命令fastlane init
;
这里有4个选项,因为我们需要的只是上传dSYM文件选择4就可以了;如果需要能自动打包并提交到AppStore功能则可以选3;选择3后续会要求配置Apple ID相关信息;
初始化成功后,项目目录下会有一个fastlane文件;
- 编辑
Fastfile
文件,编写脚本
default_platform(:ios)
platform :ios do
desc "上传到sentry"
lane :upload_symbols do
sentry_api_host = "http://sentry.io”
org_slug = “YOUR_ORG”
project_slug = “YOUR_PROJECT”
auth_token = “YOUR_TOKEN”
#download_dsyms
gym(
scheme: “YOUR_SCHEME”,
workspace: “xxxx.xcworkspace",
include_bitcode: false #根据项目bitcode设置情况
)
sentry_upload_dsym(
url: "#{sentry_api_host}",
auth_token: "#{auth_token}",
org_slug: "#{org_slug}",
project_slug: "#{project_slug}"
)
end
end
- 打包并上传
cd到项目中fastlane目录下,执行命令fastlane sentry_upload_dsym
;这步将会自动打包并拿到dSYM文件上传到sentry(省去手动打包这个步骤);
上传成功后,sentry项目设置中Debug Files就能看到文件了
之后再次捕获crash异常,查看堆栈信息就已经能解析出具体的方法了:
fastlane更多详细使用可参考:
iOS效率神器fastlane自动打包
网上没有找到关于sentry原理分析的文章,但各种异常收集框架原理大致相同,这里有篇讲解KSCrash的也可作参考;
KSCrash崩溃收集原理浅析