Sentry是一個實時事件日誌記錄和聚集的平臺。其專一於錯誤監控以及提取一切過後處理所需信息而不依賴於麻煩的用戶反饋。在國內例如Bugtags、Bugly等APP crash 採集平臺。可是Sentry 的優點在於支持服務端、Android、iOS、Web等N種平臺。還有最重要的就是他是開源的!他是開源的!他是開源的!(重要的事情要說三遍)所有平臺的SDK和服務端的代碼全是開源的! 因爲咱們公司APP最近想接入這個平臺(實際上是本人本身想私部這個平臺),因此想研究下他們的SDK,以更好的讓客戶使用咱們的產品。html
根據官方文檔,iOS的接入只須要兩個步驟:node
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!
target 'YourApp' do
pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '4.3.1'
end
複製代碼
didFinishLaunchingWithOptions
方法中初始化NSError *error = nil;
SentryClient *client = [[SentryClient alloc] initWithDsn:@"https://xxxx@sentry.io/xxxx" didFailWithError:&error];
SentryClient.sharedClient = client;
[SentryClient.sharedClient startCrashHandlerWithError:&error];
if (nil != error) {
NSLog(@"%@", error);
}
複製代碼
這樣就能夠捕獲異常並上報服務端了,簡直方便!ios
咱們看到初始化的裏面有一個關鍵性代碼(其實特麼一共也就7行代碼)c++
[SentryClient.sharedClient startCrashHandlerWithError:&error];
複製代碼
咱們startCrashHandlerWithError
函數體裏面git
- (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;
}
複製代碼
咱們看到是一個只執行一次的dispatch_once
,防止屢次初始化。從這裏開始咱們要打開新世界的大門了,關鍵性的方法調用能夠看下面的時序圖 github
咱們能夠看到最後初始化的是一系列的Monitors,而這些Monitors根據代碼中的枚舉定義一共有9種,以下:web
typedef enum
{
/* Captures and reports Mach exceptions. */
SentryCrashMonitorTypeMachException = 0x01,
/* Captures and reports POSIX signals. */
SentryCrashMonitorTypeSignal = 0x02,
/* Captures and reports C++ exceptions. * Note: This will slightly slow down exception processing. */
SentryCrashMonitorTypeCPPException = 0x04,
/* Captures and reports NSExceptions. */
SentryCrashMonitorTypeNSException = 0x08,
/* Detects and reports a deadlock in the main thread. */
SentryCrashMonitorTypeMainThreadDeadlock = 0x10,
/* Accepts and reports user-generated exceptions. */
SentryCrashMonitorTypeUserReported = 0x20,
/* Keeps track of and injects system information. */
SentryCrashMonitorTypeSystem = 0x40,
/* Keeps track of and injects application state. */
SentryCrashMonitorTypeApplicationState = 0x80,
/* Keeps track of zombies, and injects the last zombie NSException. */
SentryCrashMonitorTypeZombie = 0x100,
} SentryCrashMonitorType;
複製代碼
根據個人理解,這些Monitor能夠分紅兩類,以下圖(這裏我以文件名的形式展示類別,方便你們定位代碼)
其中CrashMonitors
指的是能夠捕捉到Crash的監控,另一種ContextMonitors
指的是用於記錄上下文注入到日誌中的監控。可是你們注意下,這些Monitors並非都會去初始化的,根據不一樣的狀況Sentry會初始化不一樣的Monitors。例如:緩存
void sentrycrashcm_setActiveMonitors(SentryCrashMonitorType monitorTypes) {
if(sentrycrashdebug_isBeingTraced() && (monitorTypes & SentryCrashMonitorTypeDebuggerUnsafe))
{
static bool hasWarned = false;
if(!hasWarned)
{
hasWarned = true;
SentryCrashLOGBASIC_WARN(" ************************ Crash Handler Notice ************************");
SentryCrashLOGBASIC_WARN(" * App is running in a debugger. Masking out unsafe monitors. *");
SentryCrashLOGBASIC_WARN(" * This means that most crashes WILL NOT BE RECORDED while debugging! *");
SentryCrashLOGBASIC_WARN(" **********************************************************************");
}
monitorTypes &= SentryCrashMonitorTypeDebuggerSafe;
}
...
}
複製代碼
其中在debug的狀況下會啓動SentryCrashMonitorTypeDebuggerSafe
一系列的監控,其中SentryCrashMonitorTypeDebuggerSafe
宏定義以下:ruby
/** Monitors that are safe to enable in a debugger. */
#define SentryCrashMonitorTypeDebuggerSafe (SentryCrashMonitorTypeAll & (~SentryCrashMonitorTypeDebuggerUnsafe))
複製代碼
下面咱們來逐個瞭解這些Monitors是如何去捕捉異常和記錄信息的,咱們根據Sentry的枚舉定義的順序來逐個解析。舒適提示:下面的代碼可能會引發你們的不適,若是你們沒有耐心能夠分屢次閱讀。app
這是捕捉內核異常的Monitor,其中核心代碼以下
static bool installExceptionHandler() {
SentryCrashLOG_DEBUG("Installing mach exception handler.");
bool attributes_created = false;
pthread_attr_t attr;
kern_return_t kr;
int error;
const task_t thisTask = mach_task_self();
exception_mask_t mask = EXC_MASK_BAD_ACCESS |
EXC_MASK_BAD_INSTRUCTION |
EXC_MASK_ARITHMETIC |
EXC_MASK_SOFTWARE |
EXC_MASK_BREAKPOINT;
//備份現有的異常接收端口
SentryCrashLOG_DEBUG("Backing up original exception ports.");
kr = task_get_exception_ports(thisTask,
mask,
g_previousExceptionPorts.masks,
&g_previousExceptionPorts.count,
g_previousExceptionPorts.ports,
g_previousExceptionPorts.behaviors,
g_previousExceptionPorts.flavors);
if(kr != KERN_SUCCESS)
{
SentryCrashLOG_ERROR("task_get_exception_ports: %s", mach_error_string(kr));
goto failed;
}
if(g_exceptionPort == MACH_PORT_NULL)
{
//分配新端口並賦予接收權限
SentryCrashLOG_DEBUG("Allocating new port with receive rights.");
kr = mach_port_allocate(thisTask,
MACH_PORT_RIGHT_RECEIVE,
&g_exceptionPort);
if(kr != KERN_SUCCESS)
{
SentryCrashLOG_ERROR("mach_port_allocate: %s", mach_error_string(kr));
goto failed;
}
SentryCrashLOG_DEBUG("Adding send rights to port.");
kr = mach_port_insert_right(thisTask,
g_exceptionPort,
g_exceptionPort,
MACH_MSG_TYPE_MAKE_SEND);
if(kr != KERN_SUCCESS)
{
SentryCrashLOG_ERROR("mach_port_insert_right: %s", mach_error_string(kr));
goto failed;
}
}
//將新端口設置爲接受異常的端口
SentryCrashLOG_DEBUG("Installing port as exception handler.");
kr = task_set_exception_ports(thisTask,
mask,
g_exceptionPort,
EXCEPTION_DEFAULT,
THREAD_STATE_NONE);
if(kr != KERN_SUCCESS)
{
SentryCrashLOG_ERROR("task_set_exception_ports: %s", mach_error_string(kr));
goto failed;
}
//建立輔助異常線程
SentryCrashLOG_DEBUG("Creating secondary exception thread (suspended).");
pthread_attr_init(&attr);
attributes_created = true;
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
error = pthread_create(&g_secondaryPThread,
&attr,
&handleExceptions,
kThreadSecondary);
if(error != 0)
{
SentryCrashLOG_ERROR("pthread_create_suspended_np: %s", strerror(error));
goto failed;
}
g_secondaryMachThread = pthread_mach_thread_np(g_secondaryPThread);
sentrycrashmc_addReservedThread(g_secondaryMachThread);
//建立主要異常線程
SentryCrashLOG_DEBUG("Creating primary exception thread.");
error = pthread_create(&g_primaryPThread,
&attr,
&handleExceptions,
kThreadPrimary);
if(error != 0)
{
SentryCrashLOG_ERROR("pthread_create: %s", strerror(error));
goto failed;
}
pthread_attr_destroy(&attr);
g_primaryMachThread = pthread_mach_thread_np(g_primaryPThread);
sentrycrashmc_addReservedThread(g_primaryMachThread);
SentryCrashLOG_DEBUG("Mach exception handler installed.");
return true;
failed:
SentryCrashLOG_DEBUG("Failed to install mach exception handler.");
if(attributes_created)
{
pthread_attr_destroy(&attr);
}
uninstallExceptionHandler();
return false;
}
複製代碼
其中關於task_get_exception_ports
,mach_port_allocate
,task_set_exception_ports
等關於內核的函數你們可能都比較陌生,其實我也是第一次見。可是咱們查看官方的開發文檔Kernel Functions發現這些函數並無文檔描述。可是別慌!我又找到了XNU的開源倉庫darwin-xnu,其中能夠檢索到這幾個函數的說明。另外還能夠參考另一個網站,是我google出來的,方便閱讀web.mit.edu/darwin/src/…
你們有興趣能夠研究下這些內核的函數,今天咱們就點到爲止。
static bool installSignalHandler() {
SentryCrashLOG_DEBUG("Installing signal handler.");
#if SentryCrashCRASH_HAS_SIGNAL_STACK
if(g_signalStack.ss_size == 0)
{
//給新的信號處理函數棧分配內存空間
SentryCrashLOG_DEBUG("Allocating signal stack area.");
g_signalStack.ss_size = SIGSTKSZ;
g_signalStack.ss_sp = malloc(g_signalStack.ss_size);
}
//替換信號處理函數棧
SentryCrashLOG_DEBUG("Setting signal stack area.");
if(sigaltstack(&g_signalStack, NULL) != 0)
{
SentryCrashLOG_ERROR("signalstack: %s", strerror(errno));
goto failed;
}
#endif
const int* fatalSignals = sentrycrashsignal_fatalSignals();
int fatalSignalsCount = sentrycrashsignal_numFatalSignals();
if(g_previousSignalHandlers == NULL)
{
//分配內存空間保存之前的信號處理函數
SentryCrashLOG_DEBUG("Allocating memory to store previous signal handlers.");
g_previousSignalHandlers = malloc(sizeof(*g_previousSignalHandlers)
* (unsigned)fatalSignalsCount);
}
struct sigaction action = {{0}};
action.sa_flags = SA_SIGINFO | SA_ONSTACK;
#if SentryCrashCRASH_HOST_APPLE && defined(__LP64__)
action.sa_flags |= SA_64REGSET;
#endif
//將信號集初始化爲空
sigemptyset(&action.sa_mask);
//設置信號異常處理器
action.sa_sigaction = &handleSignal;
//逐個設置不一樣異常信號的處理器
for(int i = 0; i < fatalSignalsCount; i++)
{
SentryCrashLOG_DEBUG("Assigning handler for signal %d", fatalSignals[i]);
//若是設置失敗,還原以前的處理器現場
if(sigaction(fatalSignals[i], &action, &g_previousSignalHandlers[i]) != 0)
{
char sigNameBuff[30];
const char* sigName = sentrycrashsignal_signalName(fatalSignals[i]);
if(sigName == NULL)
{
snprintf(sigNameBuff, sizeof(sigNameBuff), "%d", fatalSignals[i]);
sigName = sigNameBuff;
}
SentryCrashLOG_ERROR("sigaction (%s): %s", sigName, strerror(errno));
// Try to reverse the damage
for(i--;i >= 0; i--)
{
sigaction(fatalSignals[i], &g_previousSignalHandlers[i], NULL);
}
goto failed;
}
}
SentryCrashLOG_DEBUG("Signal handlers installed.");
return true;
failed:
SentryCrashLOG_DEBUG("Failed to install signal handlers.");
return false;
}
複製代碼
其中核心API函數就是sigaction
,這裏我也爲你們準備了一份gnu的官方文檔www.gnu.org/software/li…
捕獲c++異常的Monitor,其中核心代碼以下
static void setEnabled(bool isEnabled) {
if(isEnabled != g_isEnabled)
{
g_isEnabled = isEnabled;
if(isEnabled)
{
initialize();
sentrycrashid_generate(g_eventID);
//替換異常處理函數爲CPPExceptionTerminate
g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);
}
else
{
std::set_terminate(g_originalTerminateHandler);
}
g_captureNextStackTrace = isEnabled;
}
}
複製代碼
其中std::set_terminate
函數描述能夠參看C++ 參考手冊 zh.cppreference.com/w/cpp/error…
這個你們就比較熟悉了,用於捕獲APP層的異常,核心代碼以下
static void setEnabled(bool isEnabled)
{
if(isEnabled != g_isEnabled)
{
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;
}
...
}
}
複製代碼
其中關於NSSetUncaughtExceptionHandler
的用法能夠參考蘋果官方文檔developer.apple.com/documentati…
主要捕獲主線程阻塞的異常,核心代碼以下
- (void) watchdogPulse
{
__block id blockSelf = self;
self.awaitingResponse = YES;
dispatch_async(dispatch_get_main_queue(), ^
{
[blockSelf watchdogAnswer];
});
}
- (void) watchdogAnswer
{
self.awaitingResponse = NO;
}
- (void) runMonitor
{
BOOL cancelled = NO;
do
{
// Only do a watchdog check if the watchdog interval is > 0.
// If the interval is <= 0, just idle until the user changes it.
@autoreleasepool {
NSTimeInterval sleepInterval = g_watchdogInterval;
BOOL runWatchdogCheck = sleepInterval > 0;
if(!runWatchdogCheck)
{
sleepInterval = kIdleInterval;
}
[NSThread sleepForTimeInterval:sleepInterval];
cancelled = self.monitorThread.isCancelled;
if(!cancelled && runWatchdogCheck)
{
if(self.awaitingResponse)
{
[self handleDeadlock];
}
else
{
[self watchdogPulse];
}
}
}
} while (!cancelled);
}
複製代碼
經過主線程執行block的間隔時間肯定,間隔時間超過設置的閾值即認爲主線程阻塞了。
用戶經過調用下面API函數主動上報的異常
[SentryClient.sharedClient reportUserException:<#(NSString *)name#>
reason:<#(NSString *)reason#>
language:<#(NSString *)language#>
lineOfCode:<#(NSString *)lineOfCode#>
stackTrace:<#(NSArray *)stackTrace#>
logAllThreads:<#(BOOL)logAllThreads#>
terminateProgram:<#(BOOL)terminateProgram#>];
複製代碼
主要記錄和注入系統的狀態的,例如系統版本號、內核版本、是不是模擬器等,部分代碼以下
static void initialize()
{
static bool isInitialized = false;
if(!isInitialized)
{
isInitialized = true;
...
g_systemData.kernelVersion = stringSysctl("kern.version");
g_systemData.osVersion = stringSysctl("kern.osversion");
g_systemData.isJailbroken = isJailbroken();
g_systemData.bootTime = dateSysctl("kern.boottime");
g_systemData.appStartTime = dateString(time(NULL));
g_systemData.executablePath = cString(getExecutablePath());
g_systemData.executableName = cString(infoDict[@"CFBundleExecutable"]);
g_systemData.bundleID = cString(infoDict[@"CFBundleIdentifier"]);
g_systemData.bundleName = cString(infoDict[@"CFBundleName"]);
g_systemData.bundleVersion = cString(infoDict[@"CFBundleVersion"]);
g_systemData.bundleShortVersion = cString(infoDict[@"CFBundleShortVersionString"]);
g_systemData.appID = getAppUUID();
g_systemData.cpuArchitecture = getCurrentCPUArch();
g_systemData.cpuType = sentrycrashsysctl_int32ForName("hw.cputype");
g_systemData.cpuSubType = sentrycrashsysctl_int32ForName("hw.cpusubtype");
g_systemData.binaryCPUType = header->cputype;
g_systemData.binaryCPUSubType = header->cpusubtype;
g_systemData.timezone = cString([NSTimeZone localTimeZone].abbreviation);
g_systemData.processName = cString([NSProcessInfo processInfo].processName);
g_systemData.processID = [NSProcessInfo processInfo].processIdentifier;
g_systemData.parentProcessID = getppid();
g_systemData.deviceAppHash = getDeviceAndAppHash();
g_systemData.buildType = getBuildType();
g_systemData.storageSize = getStorageSize();
g_systemData.memorySize = sentrycrashsysctl_uint64ForName("hw.memsize");
}
}
複製代碼
主要記錄和注入APP的狀態的,好比啓動時間、是否在前臺等等,比較簡單,這裏就不在累述。
跟蹤並注入殭屍對象信息,核心代碼以下
#define CREATE_ZOMBIE_HANDLER_INSTALLER(CLASS) \
static IMP g_originalDealloc_ ## CLASS; \
static void handleDealloc_ ## CLASS(id self, SEL _cmd) \
{ \
handleDealloc(self); \
typedef void (*fn)(id,SEL); \
fn f = (fn)g_originalDealloc_ ## CLASS; \
f(self, _cmd); \
} \
static void installDealloc_ ## CLASS() \
{ \
Method method = class_getInstanceMethod(objc_getClass(#CLASS), sel_registerName("dealloc")); \
g_originalDealloc_ ## CLASS = method_getImplementation(method); \
method_setImplementation(method, (IMP)handleDealloc_ ## CLASS); \
}
CREATE_ZOMBIE_HANDLER_INSTALLER(NSObject)
CREATE_ZOMBIE_HANDLER_INSTALLER(NSProxy)
static void install()
{
//分配Zombie緩存空間
unsigned cacheSize = CACHE_SIZE;
g_zombieHashMask = cacheSize - 1;
g_zombieCache = calloc(cacheSize, sizeof(*g_zombieCache));
if(g_zombieCache == NULL)
{
SentryCrashLOG_ERROR("Error: Could not allocate %u bytes of memory. SentryCrashZombie NOT installed!",
cacheSize * sizeof(*g_zombieCache));
return;
}
g_lastDeallocedException.class = objc_getClass("NSException");
g_lastDeallocedException.address = NULL;
g_lastDeallocedException.name[0] = 0;
g_lastDeallocedException.reason[0] = 0;
// Hook dealloc函數
installDealloc_NSObject();
installDealloc_NSProxy();
}
複製代碼
這裏主要經過Method Swizzling方式Hook了NSObject
和NSProxy
兩個類的dealloc
函數。其中關於Method Swizzling的原理我也爲你們準備了一個科普文章iOS黑魔法-Method Swizzling
好了,到這裏你們基本已經可以知道Sentry是如何捕捉各類Crash異常事件的了,後面將會介紹Sentry是如何記錄和發送異常事件。