異常捕獲/收集的平臺(tái)有很多,我們選用了Sentry度气;Sentry支持搭建在自己的服務(wù)器上(self-hosted)墨榄,支持多種編程語(yǔ)言,號(hào)稱是有超過(guò)5萬(wàn)家公司的100萬(wàn)名開(kāi)發(fā)人員在使用即横;Sentry提供了3種類型賬號(hào):
Developer
,Team
,Business
战授;Developer類型是免費(fèi)的,但功能有限,且異常記錄每個(gè)月最多5K條(這個(gè)數(shù)量栓始,筆者還特意測(cè)試了下臊旭,異常記錄到達(dá)5K后滋戳,記錄列表將會(huì)提示讓你升級(jí)到付費(fèi)版)娄涩;如果免費(fèi)版不能滿足要求孔轴,就需付費(fèi)使用;有條件的最好是自建服務(wù)器晋柱,異常記錄數(shù)是沒(méi)有限制的优构;關(guān)于self-hosted搭建官方文檔有詳細(xì)的教程,本文我們主要只講解iOS端集成Sentry的過(guò)程雁竞;
創(chuàng)建項(xiàng)目
Sentry官網(wǎng)注冊(cè)賬號(hào)钦椭,創(chuàng)建一個(gè)Objective-C或者Swift項(xiàng)目;
然后找到項(xiàng)目設(shè)置選擇Client Keys(DSN)
碑诉,復(fù)制DSN后面代碼中需要用到(如果是self-hosted則是Public DSN)彪腔;
代碼實(shí)現(xiàn)
pod導(dǎo)入Sentry庫(kù);
appDelegate的didFinishLaunchingWithOptions方法中啟動(dòng)sentry捕獲进栽;
- (void)startSentry {
NSError *error = nil;
// 根據(jù)DSN創(chuàng)建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; // 環(huán)境 例如:debug
[SentryClient.sharedClient enableAutomaticBreadcrumbTracking]; // 開(kāi)啟面包屑功能
SentryClient.sharedClient.maxBreadcrumbs = 30; // 面包屑最多棧數(shù)
// 用戶信息
SentryUser *user = [[SentryUser alloc] initWithUserId:guid]; // 日志記錄以此區(qū)別快毛、歸類不同用戶
user.username = userName;
user.extra = @{@"cellphone":cellphone}; // 自定義字段用戶信息
SentryClient.sharedClient.user = user;
SentryClient.sharedClient.extra = @{@"other":otherMsg}; // 自定義字段信息
至此就已經(jīng)實(shí)現(xiàn)對(duì)異常的監(jiān)聽(tīng)格嗅、捕獲了;
源碼窺探
sentry比較強(qiáng)大唠帝,監(jiān)聽(tīng)了各種各樣情況的Crash異常屯掖;從源碼中可以大致窺探其支持的Crash異常類型:
這其中包括C++、死鎖襟衰、僵尸對(duì)象等等異常贴铜;我們比較熟悉的可能就是NSException
了,它只包括Foundation框架的比如數(shù)組越界右蒲、數(shù)組,字典插入nil對(duì)象等情況阀湿;接下來(lái)我們就看下SentryCrashMonitor_NSException源碼實(shí)現(xiàn),(其他類型暫時(shí)不管,看著頭大);
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;
}
如果開(kāi)啟捕獲(調(diào)試階段sentry是不開(kāi)啟捕獲的)瑰妄,則先使用g_previousUncaughtExceptionHandler
記錄之前捕獲異常的函數(shù)指針陷嘴;然后通過(guò)NSSetUncaughtExceptionHandler設(shè)置sentry的異常捕獲函數(shù);
異常發(fā)生時(shí)就會(huì)調(diào)用函數(shù)
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);
}
}
}
主要配置一些異常的信息间坐,然后將信息存儲(chǔ)起來(lái)灾挨;以備下次啟動(dòng)應(yīng)用時(shí)再調(diào)用接口上傳這些數(shù)據(jù);最后再調(diào)用之前的捕獲異常函數(shù)竹宋,這里主要的作用就是兼容其他異常捕獲功能劳澄;因?yàn)槠渌a也可能調(diào)用了NSSetUncaughtExceptionHandler設(shè)置捕獲函數(shù);
自定義Events
除了捕獲異常蜈七,sentry還支持發(fā)送自定義的日志信息秒拔,比如網(wǎng)絡(luò)請(qǐng)求失敗就可以將失敗信息上傳;
SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentrySeverityWarning]; // 指定事件的嚴(yán)重級(jí)別 Fatal/Error/Warnig
event.message = message; // 錯(cuò)誤信息
event.environment = environment;
event.extra =@{@"url":url,@"code":@(code),@"param":param}; // 自定義字段
[SentryClient.sharedClient sendEvent:event withCompletionHandler:^(NSError * _Nullable error) {
if (nil != error) {
NSLog(@"%@", error);
}
}];
測(cè)試
sentry代碼都寫好后飒硅,我們手動(dòng)拋個(gè)異常測(cè)下sentry平臺(tái)是否能正常統(tǒng)計(jì)異常的數(shù)據(jù)砂缩;
一切沒(méi)問(wèn)題的話作谚,后臺(tái)我們就能看到日志記錄了:
- 面包屑信息
- 方法調(diào)用棧
因?yàn)槲覀冞€沒(méi)有上傳dSYM符號(hào)文件,sentry不能解析crash日志定位到具體方法庵芭;
上傳dSYM文件
sentry平臺(tái)創(chuàng)建Token:User Setting ----> Auth Tokens ---> Create New Token妹懒;創(chuàng)建時(shí)按需勾選選項(xiàng);
拿到Token后就可以上傳文件了
有兩種上傳方式
- shell腳本上傳
- 先打包双吆,然后拿到dSYM文件眨唬;
- 安裝sentry-cli:
brew install getsentry/tools/sentry-cli - 編寫并執(zhí)行以下腳本即可,其中
URL
如果是sentry服務(wù)器則是https://sentry.io
好乐,如果是self-hosted則填寫自己服務(wù)器url匾竿;
sentry-cli --url URL --auth-token YOUR_TOKEN upload-dif --org YOUR_ORG --project YOUR_PROJECT dSYM_PATH --log-level=debug
- 通過(guò)fastlane上傳
- 安裝fastlane
sudo gem install fastlane -NV
或是brew cask install fastlane
命令安裝;
安裝完后執(zhí)行命令fastlane --version
蔚万,確認(rèn)安裝成功搂橙; - 初始化fastlane
cd到項(xiàng)目目錄下,執(zhí)行命令fastlane init
;
這里有4個(gè)選項(xiàng)笛坦,因?yàn)槲覀冃枰闹皇巧蟼鱠SYM文件選擇4就可以了;如果需要能自動(dòng)打包并提交到AppStore功能則可以選3苔巨;選擇3后續(xù)會(huì)要求配置Apple ID相關(guān)信息版扩;
初始化成功后,項(xiàng)目目錄下會(huì)有一個(gè)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 #根據(jù)項(xiàng)目bitcode設(shè)置情況
)
sentry_upload_dsym(
url: "#{sentry_api_host}",
auth_token: "#{auth_token}",
org_slug: "#{org_slug}",
project_slug: "#{project_slug}"
)
end
end
- 打包并上傳
cd到項(xiàng)目中fastlane目錄下沟于,執(zhí)行命令fastlane sentry_upload_dsym
;這步將會(huì)自動(dòng)打包并拿到dSYM文件上傳到sentry(省去手動(dòng)打包這個(gè)步驟);
上傳成功后膘流,sentry項(xiàng)目設(shè)置中Debug Files就能看到文件了
之后再次捕獲crash異常,查看堆棧信息就已經(jīng)能解析出具體的方法了:
fastlane更多詳細(xì)使用可參考:
iOS效率神器fastlane自動(dòng)打包
網(wǎng)上沒(méi)有找到關(guān)于sentry原理分析的文章鸟缕,但各種異常收集框架原理大致相同鸭限,這里有篇講解KSCrash的也可作參考;
KSCrash崩潰收集原理淺析