七、 Crash 监控
1. 异常相关知识回顾
1.1 Mach 层对异常的处理
Mach 在消息传递基础上实现了一套独特的异常处理方法。Mach 异常处理在设计时考虑到:
- 带有一致的语义的单一异常处理设施:Mach 只提供一个异常处理机制用于处理所有类型的异常(包括用户定义的异常、平台无关的异常以及平台特定的异常)。根据异常类型进行分组,具体的平台可以定义具体的子类型。
- 清晰和简洁:异常处理的接口依赖于 Mach 已有的具有良好定义的消息和端口架构,因此非常优雅(不会影响效率)。这就允许调试器和外部处理程序的拓展-甚至在理论上还支持拓展基于网络的异常处理。
在 Mach 中,异常是通过内核中的基础设施-消息传递机制处理的。一个异常并不比一条消息复杂多少,异常由出错的线程或者任务(通过 msg_send()) 抛出,然后由一个处理程序通过 msg_recv())捕捉。处理程序可以处理异常,也可以清楚异常(将异常标记为已完成并继续),还可以决定终止线程。
Mach 的异常处理模型和其他的异常处理模型不同,其他模型的异常处理程序运行在出错的线程上下文中,而 Mach 的异常处理程序在不同的上下文中运行异常处理程序,出错的线程向预先指定好的异常端口发送消息,然后等待应答。每一个任务都可以注册一个异常处理端口,这个异常处理端口会对该任务中的所有线程生效。此外,每个线程都可以通过 thread_set_exception_ports(<#thread_act_t thread#>, <#exception_mask_t exception_mask#>, <#mach_port_t new_port#>, <#exception_behavior_t behavior#>, <#thread_state_flavor_t new_flavor#>)
注册自己的异常处理端口。通常情况下,任务和线程的异常端口都是 NULL,也就是异常不会被处理,而一旦创建异常端口,这些端口就像系统中的其他端口一样,可以转交给其他任务或者其他主机。(有了端口,就可以使用 UDP 协议,通过网络能力让其他的主机上应用程序处理异常)。
发生异常时,首先尝试将异常抛给线程的异常端口,然后尝试抛给任务的异常端口,最后再抛给主机的异常端口(即主机注册的默认端口)。如果没有一个端口返回 KERN_SUCCESS
,那么整个任务将被终止。也就是 Mach 不提供异常处理逻辑,只提供传递异常通知的框架。
异常首先是由处理器陷阱引发的。为了处理陷阱,每一个现代的内核都会安插陷阱处理程序。这些底层函数是由内核的汇编部分安插的。
1.2 BSD 层对异常的处理
BSD 层是用户态主要使用的 XUN 接口,这一层展示了一个符合 POSIX 标准的接口。开发者可以使用 UNIX 系统的一切功能,但不需要了解 Mach 层的细节实现。
Mach 已经通过异常机制提供了底层的陷进处理,而 BSD 则在异常机制之上构建了信号处理机制。硬件产生的信号被 Mach 层捕捉,然后转换为对应的 UNIX 信号,为了维护一个统一的机制,操作系统和用户产生的信号首先被转换为 Mach 异常,然后再转换为信号。
Mach 异常都在 host 层被 ux_exception
转换为相应的 unix 信号,并通过 threadsignal
将信号投递到出错的线程。
2. Crash 收集方式
iOS 系统自带的 Apples`s Crash Reporter 在设置中记录 Crash 日志,我们先观察下 Crash 日志
Incident Identifier: 7FA6736D-09E8-47A1-95EC-76C4522BDE1A
CrashReporter Key: 4e2d36419259f14413c3229e8b7235bcc74847f3
Hardware Model: iPhone7,1
Process: APMMonitorExample [3608]
Path: /var/containers/Bundle/Application/9518A4F4-59B7-44E9-BDDA-9FBEE8CA18E5/APMMonitorExample.app/APMMonitorExample
Identifier: com.Wacai.APMMonitorExample
Version: 1.0 (1)
Code Type: ARM-64
Parent Process: ? [1]
Date/Time: 2017-01-03 11:43:03.000 +0800
OS Version: iOS 10.2 (14C92)
Report Version: 104
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x00000000 at 0x0000000000000000
Crashed Thread: 0
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSSingleObjectArrayI objectForKey:]: unrecognized selector sent to instance 0x174015060'
Thread 0 Crashed:
0 CoreFoundation 0x0000000188f291b8 0x188df9000 + 1245624 (<redacted> + 124)
1 libobjc.A.dylib 0x000000018796055c 0x187958000 + 34140 (objc_exception_throw + 56)
2 CoreFoundation 0x0000000188f30268 0x188df9000 + 1274472 (<redacted> + 140)
3 CoreFoundation 0x0000000188f2d270 0x188df9000 + 1262192 (<redacted> + 916)
4 CoreFoundation 0x0000000188e2680c 0x188df9000 + 186380 (_CF_forwarding_prep_0 + 92)
5 APMMonitorExample 0x000000010004c618 0x100044000 + 34328 (-[MakeCrashHandler throwUncaughtNSException] + 80)
会发现,Crash 日志中 Exception Type
项由2部分组成:Mach 异常 + Unix 信号。
所以 Exception Type: EXC_CRASH (SIGABRT)
表示:Mach 层发生了 EXC_CRASH
异常,在 host 层被转换为 SIGABRT
信号投递到出错的线程。
问题: 捕获 Mach 层异常、注册 Unix 信号处理都可以捕获 Crash,这两种方式如何选择?
答: 优选 Mach 层异常拦截。根据上面 1.2 中的描述我们知道 Mach 层异常处理时机更早些,假如 Mach 层异常处理程序让进程退出,这样 Unix 信号永远不会发生了。
业界关于崩溃日志的收集开源项目很多,著名的有: KSCrash、plcrashreporter,提供一条龙服务的 Bugly、友盟等。我们一般使用开源项目在此基础上开发成符合公司内部需求的 bug 收集工具。一番对比后选择 KSCrash。为什么选择 KSCrash 不在本文重点。
KSCrash 功能齐全,可以捕获如下类型的 Crash
- Mach kernel exceptions
- Fatal signals
- C++ exceptions
- Objective-C exceptions
- Main thread deadlock (experimental)
- Custom crashes (e.g. from scripting languages)
所以分析 iOS 端的 Crash 收集方案也就是分析 KSCrash 的 Crash 监控实现原理。
2.1. Mach 层异常处理
大体思路是:先创建一个异常处理端口,为该端口申请权限,再设置异常端口、新建一个内核线程,在该线程内循环等待异常。但是为了防止自己注册的 Mach 层异常处理抢占了其他 SDK、或者业务线开发者设置的逻辑,我们需要在最开始保存其他的异常处理端口,等逻辑执行完后将异常处理交给其他的端口内的逻辑处理。收集到 Crash 信息后组装数据,写入 json 文件。
流程图如下:
对于 Mach 异常捕获,可以注册一个异常端口,该端口负责对当前任务的所有线程进行监听。
下面来看看关键代码:
注册 Mach 层异常监听代码
static bool installExceptionHandler()
{
KSLOG_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;
KSLOG_DEBUG("Backing up original exception ports.");
// 获取该 Task 上的注册好的异常端口
kr = task_get_exception_ports(thisTask,
mask,
g_previousExceptionPorts.masks,
&g_previousExceptionPorts.count,
g_previousExceptionPorts.ports,
g_previousExceptionPorts.behaviors,
g_previousExceptionPorts.flavors);
// 获取失败走 failed 逻辑
if(kr != KERN_SUCCESS)
{
KSLOG_ERROR("task_get_exception_ports: %s", mach_error_string(kr));
goto failed;
}
// KSCrash 的异常为空则走执行逻辑
if(g_exceptionPort == MACH_PORT_NULL)
{
KSLOG_DEBUG("Allocating new port with receive rights.");
// 申请异常处理端口
kr = mach_port_allocate(thisTask,
MACH_PORT_RIGHT_RECEIVE,
&g_exceptionPort);
if(kr != KERN_SUCCESS)
{
KSLOG_ERROR("mach_port_allocate: %s", mach_error_string(kr));
goto failed;
}
KSLOG_DEBUG("Adding send rights to port.");
// 为异常处理端口申请权限:MACH_MSG_TYPE_MAKE_SEND
kr = mach_port_insert_right(thisTask,
g_exceptionPort,
g_exceptionPort,
MACH_MSG_TYPE_MAKE_SEND);
if(kr != KERN_SUCCESS)
{
KSLOG_ERROR("mach_port_insert_right: %s", mach_error_string(kr));
goto failed;
}
}
KSLOG_DEBUG("Installing port as exception handler.");
// 为该 Task 设置异常处理端口
kr = task_set_exception_ports(thisTask,
mask,
g_exceptionPort,
EXCEPTION_DEFAULT,
THREAD_STATE_NONE);
if(kr != KERN_SUCCESS)
{
KSLOG_ERROR("task_set_exception_ports: %s", mach_error_string(kr));
goto failed;
}
KSLOG_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)
{
KSLOG_ERROR("pthread_create_suspended_np: %s", strerror(error));
goto failed;
}
// 转换为 Mach 内核线程
g_secondaryMachThread = pthread_mach_thread_np(g_secondaryPThread);
ksmc_addReservedThread(g_secondaryMachThread);
KSLOG_DEBUG("Creating primary exception thread.");
error = pthread_create(&g_primaryPThread,
&attr,
&handleExceptions,
kThreadPrimary);
if(error != 0)
{
KSLOG_ERROR("pthread_create: %s", strerror(error));
goto failed;
}
pthread_attr_destroy(&attr);
g_primaryMachThread = pthread_mach_thread_np(g_primaryPThread);
ksmc_addReservedThread(g_primaryMachThread);
KSLOG_DEBUG("Mach exception handler installed.");
return true;
failed:
KSLOG_DEBUG("Failed to install mach exception handler.");
if(attributes_created)
{
pthread_attr_destroy(&attr);
}
// 还原之前的异常注册端口,将控制权还原
uninstallExceptionHandler();
return false;
}
处理异常的逻辑、组装崩溃信息
/** Our exception handler thread routine.
* Wait for an exception message, uninstall our exception port, record the
* exception information, and write a report.
*/
static void* handleExceptions(void* const userData)
{
MachExceptionMessage exceptionMessage = {{0}};
MachReplyMessage replyMessage = {{0}};
char* eventID = g_primaryEventID;
const char* threadName = (const char*) userData;
pthread_setname_np(threadName);
if(threadName == kThreadSecondary)
{
KSLOG_DEBUG("This is the secondary thread. Suspending.");
thread_suspend((thread_t)ksthread_self());
eventID = g_secondaryEventID;
}
// 循环读取注册好的异常端口信息
for(;;)
{
KSLOG_DEBUG("Waiting for mach exception");
// Wait for a message.
kern_return_t kr = mach_msg(&exceptionMessage.header,
MACH_RCV_MSG,
0,
sizeof(exceptionMessage),
g_exceptionPort,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
// 获取到信息后则代表发生了 Mach 层异常,跳出 for 循环,组装数据
if(kr == KERN_SUCCESS)
{
break;
}
// Loop and try again on failure.
KSLOG_ERROR("mach_msg: %s", mach_error_string(kr));
}
KSLOG_DEBUG("Trapped mach exception code 0x%x, subcode 0x%x",
exceptionMessage.code[0], exceptionMessage.code[1]);
if(g_isEnabled)
{
// 挂起所有线程
ksmc_suspendEnvironment();
g_isHandlingCrash = true;
// 通知发生了异常
kscm_notifyFatalExceptionCaptured(true);
KSLOG_DEBUG("Exception handler is installed. Continuing exception handling.");
// Switch to the secondary thread if necessary, or uninstall the handler
// to avoid a death loop.
if(ksthread_self() == g_primaryMachThread)
{
KSLOG_DEBUG("This is the primary exception thread. Activating secondary thread.");
// TODO: This was put here to avoid a freeze. Does secondary thread ever fire?
restoreExceptionPorts();
if(thread_resume(g_secondaryMachThread) != KERN_SUCCESS)
{
KSLOG_DEBUG("Could not activate secondary thread. Restoring original exception ports.");
}
}
else
{
KSLOG_DEBUG("This is the secondary exception thread. Restoring original exception ports.");
// restoreExceptionPorts();
}
// Fill out crash information
// 组装异常所需要的方案现场信息
KSLOG_DEBUG("Fetching machine state.");
KSMC_NEW_CONTEXT(machineContext);
KSCrash_MonitorContext* crashContext = &g_monitorContext;
crashContext->offendingMachineContext = machineContext;
kssc_initCursor(&g_stackCursor, NULL, NULL);
if(ksmc_getContextForThread(exceptionMessage.thread.name, machineContext, true))
{
kssc_initWithMachineContext(&g_stackCursor, 100, machineContext);
KSLOG_TRACE("Fault address 0x%x, instruction address 0x%x", kscpu_faultAddress(machineContext), kscpu_instructionAddress(machineContext));
if(exceptionMessage.exception == EXC_BAD_ACCESS)
{
crashContext->faultAddress = kscpu_faultAddress(machineContext);
}
else
{
crashContext->faultAddress = kscpu_instructionAddress(machineContext);
}
}
KSLOG_DEBUG("Filling out context.");
crashContext->crashType = KSCrashMonitorTypeMachException;
crashContext->eventID = eventID;
crashContext->registersAreValid = true;
crashContext->mach.type = exceptionMessage.exception;
crashContext->mach.code = exceptionMessage.code[0];
crashContext->mach.subcode = exceptionMessage.code[1];
if(crashContext->mach.code == KERN_PROTECTION_FAILURE && crashContext->isStackOverflow)
{
// A stack overflow should return KERN_INVALID_ADDRESS, but
// when a stack blasts through the guard pages at the top of the stack,
// it generates KERN_PROTECTION_FAILURE. Correct for this.
crashContext->mach.code = KERN_INVALID_ADDRESS;
}
crashContext->signal.signum = signalForMachException(crashContext->mach.type, crashContext->mach.code);
crashContext->stackCursor = &g_stackCursor;
kscm_handleException(crashContext);
KSLOG_DEBUG("Crash handling complete. Restoring original handlers.");
g_isHandlingCrash = false;
ksmc_resumeEnvironment();
}
KSLOG_DEBUG("Replying to mach exception message.");
// Send a reply saying "I didn't handle this exception".
replyMessage.header = exceptionMessage.header;
replyMessage.NDR = exceptionMessage.NDR;
replyMessage.returnCode = KERN_FAILURE;
mach_msg(&replyMessage.header,
MACH_SEND_MSG,
sizeof(replyMessage),
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
return NULL;
}
还原异常处理端口,转移控制权
/** Restore the original mach exception ports.
*/
static void restoreExceptionPorts(void)
{
KSLOG_DEBUG("Restoring original exception ports.");
if(g_previousExceptionPorts.count == 0)
{
KSLOG_DEBUG("Original exception ports were already restored.");
return;
}
const task_t thisTask = mach_task_self();
kern_return_t kr;
// Reinstall old exception ports.
// for 循环去除保存好的在 KSCrash 之前注册好的异常端口,将每个端口注册回去
for(mach_msg_type_number_t i = 0; i < g_previousExceptionPorts.count; i++)
{
KSLOG_TRACE("Restoring port index %d", i);
kr = task_set_exception_ports(thisTask,
g_previousExceptionPorts.masks[i],
g_previousExceptionPorts.ports[i],
g_previousExceptionPorts.behaviors[i],
g_previousExceptionPorts.flavors[i]);
if(kr != KERN_SUCCESS)
{
KSLOG_ERROR("task_set_exception_ports: %s",
mach_error_string(kr));
}
}
KSLOG_DEBUG("Exception ports restored.");
g_previousExceptionPorts.count = 0;
}
2.2. Signal 异常处理
对于 Mach 异常,操作系统会将其转换为对应的 Unix 信号
,所以开发者可以通过注册 signanHandler
的方式来处理。
KSCrash 在这里的处理逻辑如下图:
看一下关键代码:
设置信号处理函数
static bool installSignalHandler()
{
KSLOG_DEBUG("Installing signal handler.");
#if KSCRASH_HAS_SIGNAL_STACK
// 在堆上分配一块内存,
if(g_signalStack.ss_size == 0)
{
KSLOG_DEBUG("Allocating signal stack area.");
g_signalStack.ss_size = SIGSTKSZ;
g_signalStack.ss_sp = malloc(g_signalStack.ss_size);
}
// 信号处理函数的栈挪到堆中,而不和进程共用一块栈区
// sigaltstack() 函数,该函数的第 1 个参数 sigstack 是一个 stack_t 结构的指针,该结构存储了一个“可替换信号栈” 的位置及属性信息。第 2 个参数 old_sigstack 也是一个 stack_t 类型指针,它用来返回上一次建立的“可替换信号栈”的信息(如果有的话)
KSLOG_DEBUG("Setting signal stack area.");
// sigaltstack 第一个参数为创建的新的可替换信号栈,第二个参数可以设置为NULL,如果不为NULL的话,将会将旧的可替换信号栈的信息保存在里面。函数成功返回0,失败返回-1.
if(sigaltstack(&g_signalStack, NULL) != 0)
{
KSLOG_ERROR("signalstack: %s", strerror(errno));
goto failed;
}
#endif
const int* fatalSignals = kssignal_fatalSignals();
int fatalSignalsCount = kssignal_numFatalSignals();
if(g_previousSignalHandlers == NULL)
{
KSLOG_DEBUG("Allocating memory to store previous signal handlers.");
g_previousSignalHandlers = malloc(sizeof(*g_previousSignalHandlers)
* (unsigned)fatalSignalsCount);
}
// 设置信号处理函数 sigaction 的第二个参数,类型为 sigaction 结构体
struct sigaction action = {{0}};
// sa_flags 成员设立 SA_ONSTACK 标志,该标志告诉内核信号处理函数的栈帧就在“可替换信号栈”上建立。
action.sa_flags = SA_SIGINFO | SA_ONSTACK;
#if KSCRASH_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++)
{
// 将每个信号的处理函数绑定到上面声明的 action 去,另外用 g_previousSignalHandlers 保存当前信号的处理函数
KSLOG_DEBUG("Assigning handler for signal %d", fatalSignals[i]);
if(sigaction(fatalSignals[i], &action, &g_previousSignalHandlers[i]) != 0)
{
char sigNameBuff[30];
const char* sigName = kssignal_signalName(fatalSignals[i]);
if(sigName == NULL)
{
snprintf(sigNameBuff, sizeof(sigNameBuff), "%d", fatalSignals[i]);
sigName = sigNameBuff;
}
KSLOG_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;
}
}
KSLOG_DEBUG("Signal handlers installed.");
return true;
failed:
KSLOG_DEBUG("Failed to install signal handlers.");
return false;
}
信号处理时记录线程等上下文信息
static void handleSignal(int sigNum, siginfo_t* signalInfo, void* userContext)
{
KSLOG_DEBUG("Trapped signal %d", sigNum);
if(g_isEnabled)
{
ksmc_suspendEnvironment();
kscm_notifyFatalExceptionCaptured(false);
KSLOG_DEBUG("Filling out context.");
KSMC_NEW_CONTEXT(machineContext);
ksmc_getContextForSignal(userContext, machineContext);
kssc_initWithMachineContext(&g_stackCursor, 100, machineContext);
// 记录信号处理时的上下文信息
KSCrash_MonitorContext* crashContext = &g_monitorContext;
memset(crashContext, 0, sizeof(*crashContext));
crashContext->crashType = KSCrashMonitorTypeSignal;
crashContext->eventID = g_eventID;
crashContext->offendingMachineContext = machineContext;
crashContext->registersAreValid = true;
crashContext->faultAddress = (uintptr_t)signalInfo->si_addr;
crashContext->signal.userContext = userContext;
crashContext->signal.signum = signalInfo->si_signo;
crashContext->signal.sigcode = signalInfo->si_code;
crashContext->stackCursor = &g_stackCursor;
kscm_handleException(crashContext);
ksmc_resumeEnvironment();
}
KSLOG_DEBUG("Re-raising signal for regular handlers to catch.");
// This is technically not allowed, but it works in OSX and iOS.
raise(sigNum);
}
KSCrash 信号处理后还原之前的信号处理权限
static void uninstallSignalHandler(void)
{
KSLOG_DEBUG("Uninstalling signal handlers.");
const int* fatalSignals = kssignal_fatalSignals();
int fatalSignalsCount = kssignal_numFatalSignals();
// 遍历需要处理信号数组,将之前的信号处理函数还原
for(int i = 0; i < fatalSignalsCount; i++)
{
KSLOG_DEBUG("Restoring original handler for signal %d", fatalSignals[i]);
sigaction(fatalSignals[i], &g_previousSignalHandlers[i], NULL);
}
KSLOG_DEBUG("Signal handlers uninstalled.");
}
说明:
-
先从堆上分配一块内存区域,被称为“可替换信号栈”,目的是将信号处理函数的栈干掉,用堆上的内存区域代替,而不和进程共用一块栈区。
为什么这么做?一个进程可能有 n 个线程,每个线程都有自己的任务,假如某个线程执行出错,这样就会导致整个进程的崩溃。所以为了信号处理函数正常运行,需要为信号处理函数设置单独的运行空间。另一种情况是递归函数将系统默认的栈空间用尽了,但是信号处理函数使用的栈是它实现在堆中分配的空间,而不是系统默认的栈,所以它仍旧可以正常工作。
-
int sigaltstack(const stack_t * __restrict, stack_t * __restrict)
函数的二个参数都是stack_t
结构的指针,存储了可替换信号栈的信息(栈的起始地址、栈的长度、状态)。第1个参数该结构存储了一个“可替换信号栈” 的位置及属性信息。第 2 个参数用来返回上一次建立的“可替换信号栈”的信息(如果有的话)。_STRUCT_SIGALTSTACK { void *ss_sp; /* signal stack base */ __darwin_size_t ss_size; /* signal stack length */ int ss_flags; /* SA_DISABLE and/or SA_ONSTACK */ }; typedef _STRUCT_SIGALTSTACK stack_t; /* [???] signal stack */
新创建的可替换信号栈,
ss_flags
必须设置为 0。系统定义了SIGSTKSZ
常量,可满足绝大多可替换信号栈的需求。/* * Structure used in sigaltstack call. */ #define SS_ONSTACK 0x0001 /* take signal on signal stack */ #define SS_DISABLE 0x0004 /* disable taking signals on alternate stack */ #define MINSIGSTKSZ 32768 /* (32K)minimum allowable stack */ #define SIGSTKSZ 131072 /* (128K)recommended stack size */
sigaltstack
系统调用通知内核“可替换信号栈”已经建立。ss_flags
为SS_ONSTACK
时,表示进程当前正在“可替换信号栈”中执行,如果此时试图去建立一个新的“可替换信号栈”,那么会遇到EPERM
(禁止该动作) 的错误;为SS_DISABLE
说明当前没有已建立的“可替换信号栈”,禁止建立“可替换信号栈”。 -
int sigaction(int, const struct sigaction * __restrict, struct sigaction * __restrict);
第一个函数表示需要处理的信号值,但不能是
SIGKILL
和SIGSTOP
,这两个信号的处理函数不允许用户重写,因为它们给超级用户提供了终止程序的方法(SIGKILL
andSIGSTOP
cannot be caught, blocked, or ignored);第二个和第三个参数是一个
sigaction
结构体。如果第二个参数不为空则代表将其指向信号处理函数,第三个参数不为空,则将之前的信号处理函数保存到该指针中。如果第二个参数为空,第三个参数不为空,则可以获取当前的信号处理函数。/* * Signal vector "template" used in sigaction call. */ struct sigaction { union __sigaction_u __sigaction_u; /* signal handler */ sigset_t sa_mask; /* signal mask to apply */ int sa_flags; /* see signal options below */ };
sigaction
函数的sa_flags
参数需要设置SA_ONSTACK
标志,告诉内核信号处理函数的栈帧就在“可替换信号栈”上建立。
2.3. C++ 异常处理
c++ 异常处理的实现是依靠了标准库的 std::set_terminate(CPPExceptionTerminate)
函数。
iOS 工程中某些功能的实现可能使用了C、C++等。假如抛出 C++ 异常,如果该异常可以被转换为 NSException,则走 OC 异常捕获机制,如果不能转换,则继续走 C++ 异常流程,也就是 default_terminate_handler
。这个 C++ 异常的默认 terminate 函数内部调用 abort_message
函数,最后触发了一个 abort
调用,系统产生一个 SIGABRT
信号。
在系统抛出 C++ 异常后,加一层 try...catch...
来判断该异常是否可以转换为 NSException
,再重新抛出的C++异常。此时异常的现场堆栈已经消失,所以上层通过捕获 SIGABRT
信号是无法还原发生异常时的场景,即异常堆栈缺失。
为什么?try...catch...
语句内部会调用 __cxa_rethrow()
抛出异常,__cxa_rethrow()
内部又会调用 unwind
,unwind
可以简单理解为函数调用的逆调用,主要用来清理函数调用过程中每个函数生成的局部变量,一直到最外层的 catch 语句所在的函数,并把控制移交给 catch 语句,这就是C++异常的堆栈消失原因。
static void setEnabled(bool isEnabled)
{
if(isEnabled != g_isEnabled)
{
g_isEnabled = isEnabled;
if(isEnabled)
{
initialize();
ksid_generate(g_eventID);
g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);
}
else
{
std::set_terminate(g_originalTerminateHandler);
}
g_captureNextStackTrace = isEnabled;
}
}
static void initialize()
{
static bool isInitialized = false;
if(!isInitialized)
{
isInitialized = true;
kssc_initCursor(&g_stackCursor, NULL, NULL);
}
}
void kssc_initCursor(KSStackCursor *cursor,
void (*resetCursor)(KSStackCursor*),
bool (*advanceCursor)(KSStackCursor*))
{
cursor->symbolicate = kssymbolicator_symbolicate;
cursor->advanceCursor = advanceCursor != NULL ? advanceCursor : g_advanceCursor;
cursor->resetCursor = resetCursor != NULL ? resetCursor : kssc_resetCursor;
cursor->resetCursor(cursor);
}
static void CPPExceptionTerminate(void)
{
ksmc_suspendEnvironment();
KSLOG_DEBUG("Trapped c++ exception");
const char* name = NULL;
std::type_info* tinfo = __cxxabiv1::__cxa_current_exception_type();
if(tinfo != NULL)
{
name = tinfo->name();
}
if(name == NULL || strcmp(name, "NSException") != 0)
{
kscm_notifyFatalExceptionCaptured(false);
KSCrash_MonitorContext* crashContext = &g_monitorContext;
memset(crashContext, 0, sizeof(*crashContext));
char descriptionBuff[DESCRIPTION_BUFFER_LENGTH];
const char* description = descriptionBuff;
descriptionBuff[0] = 0;
KSLOG_DEBUG("Discovering what kind of exception was thrown.");
g_captureNextStackTrace = false;
try
{
throw;
}
catch(std::exception& exc)
{
strncpy(descriptionBuff, exc.what(), sizeof(descriptionBuff));
}
#define CATCH_VALUE(TYPE, PRINTFTYPE) \
catch(TYPE value)\
{ \
snprintf(descriptionBuff, sizeof(descriptionBuff), "%" #PRINTFTYPE, value); \
}
CATCH_VALUE(char, d)
CATCH_VALUE(short, d)
CATCH_VALUE(int, d)
CATCH_VALUE(long, ld)
CATCH_VALUE(long long, lld)
CATCH_VALUE(unsigned char, u)
CATCH_VALUE(unsigned short, u)
CATCH_VALUE(unsigned int, u)
CATCH_VALUE(unsigned long, lu)
CATCH_VALUE(unsigned long long, llu)
CATCH_VALUE(float, f)
CATCH_VALUE(double, f)
CATCH_VALUE(long double, Lf)
CATCH_VALUE(char*, s)
catch(...)
{
description = NULL;
}
g_captureNextStackTrace = g_isEnabled;
// TODO: Should this be done here? Maybe better in the exception handler?
KSMC_NEW_CONTEXT(machineContext);
ksmc_getContextForThread(ksthread_self(), machineContext, true);
KSLOG_DEBUG("Filling out context.");
crashContext->crashType = KSCrashMonitorTypeCPPException;
crashContext->eventID = g_eventID;
crashContext->registersAreValid = false;
crashContext->stackCursor = &g_stackCursor;
crashContext->CPPException.name = name;
crashContext->exceptionName = name;
crashContext->crashReason = description;
crashContext->offendingMachineContext = machineContext;
kscm_handleException(crashContext);
}
else
{
KSLOG_DEBUG("Detected NSException. Letting the current NSException handler deal with it.");
}
ksmc_resumeEnvironment();
KSLOG_DEBUG("Calling original terminate handler.");
g_originalTerminateHandler();
}
2.4. Objective-C 异常处理
对于 OC 层面的 NSException 异常处理较为容易,可以通过注册 NSUncaughtExceptionHandler
来捕获异常信息,通过 NSException 参数来做 Crash 信息的收集,交给数据上报组件。
static void setEnabled(bool isEnabled)
{
if(isEnabled != g_isEnabled)
{
g_isEnabled = isEnabled;
if(isEnabled)
{
KSLOG_DEBUG(@"Backing up original handler.");
// 记录之前的 OC 异常处理函数
g_previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
KSLOG_DEBUG(@"Setting new handler.");
// 设置新的 OC 异常处理函数
NSSetUncaughtExceptionHandler(&handleException);
KSCrash.sharedInstance.uncaughtExceptionHandler = &handleException;
}
else
{
KSLOG_DEBUG(@"Restoring original handler.");
NSSetUncaughtExceptionHandler(g_previousUncaughtExceptionHandler);
}
}
}
2.5. 主线程死锁
主线程死锁的检测和 ANR 的检测有些类似
-
创建一个线程,在线程运行方法中用
do...while...
循环处理逻辑,加了 autorelease 避免内存过高 -
有一个
awaitingResponse
属性和watchdogPulse
方法。watchdogPulse 主要逻辑为设置awaitingResponse
为 YES,切换到主线程中,设置awaitingResponse
为 NO,- (void) watchdogPulse { __block id blockSelf = self; self.awaitingResponse = YES; dispatch_async(dispatch_get_main_queue(), ^ { [blockSelf watchdogAnswer]; }); }
-
线程的执行方法里面不断循环,等待设置的
g_watchdogInterval
后判断awaitingResponse
的属性值是不是初始状态的值,否则判断为死锁- (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); }
2.6 Crash 的生成与保存
2.6.1 Crash 日志的生成逻辑
上面的部分讲过了 iOS 应用开发中的各种 crash 监控逻辑,接下来就应该分析下 crash 捕获后如何将 crash 信息记录下来,也就是保存到应用沙盒中。
拿主线程死锁这种 crash 举例子,看看 KSCrash 是如何记录 crash 信息的。
// KSCrashMonitor_Deadlock.m
- (void) handleDeadlock
{
ksmc_suspendEnvironment();
kscm_notifyFatalExceptionCaptured(false);
KSMC_NEW_CONTEXT(machineContext);
ksmc_getContextForThread(g_mainQueueThread, machineContext, false);
KSStackCursor stackCursor;
kssc_initWithMachineContext(&stackCursor, 100, machineContext);
char eventID[37];
ksid_generate(eventID);
KSLOG_DEBUG(@"Filling out context.");
KSCrash_MonitorContext* crashContext = &g_monitorContext;
memset(crashContext, 0, sizeof(*crashContext));
crashContext->crashType = KSCrashMonitorTypeMainThreadDeadlock;
crashContext->eventID = eventID;
crashContext->registersAreValid = false;
crashContext->offendingMachineContext = machineContext;
crashContext->stackCursor = &stackCursor;
kscm_handleException(crashContext);
ksmc_resumeEnvironment();
KSLOG_DEBUG(@"Calling abort()");
abort();
}
其他几个 crash 也是一样,异常信息经过包装交给 kscm_handleException()
函数处理。可以看到这个函数被其他几种 crash 捕获后所调用。
/** Start general exception processing.
*
* @oaram context Contextual information about the exception.
*/
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];
// 判断当前的 crash 监控是开启状态
if(isMonitorEnabled(monitor))
{
// 针对每种 crash 类型做一些额外的补充信息
addContextualInfoToEvent(monitor, context);
}
}
// 真正处理 crash 信息,保存 json 格式的 crash 信息
g_onExceptionEvent(context);
if(g_handlingFatalException && !g_crashedDuringExceptionHandling)
{
KSLOG_DEBUG("Exception is fatal. Restoring original handlers.");
kscm_setActiveMonitors(KSCrashMonitorTypeNone);
}
}
g_onExceptionEvent
是一个 block,声明为 static void (*g_onExceptionEvent)(struct KSCrash_MonitorContext* monitorContext);
在 KSCrashMonitor.c
中被赋值
void kscm_setEventCallback(void (*onEvent)(struct KSCrash_MonitorContext* monitorContext))
{
g_onExceptionEvent = onEvent;
}
kscm_setEventCallback()
函数在 KSCrashC.c
文件中被调用
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);
// 设置 crash 发生时的 callback 函数
kscm_setEventCallback(onCrash);
KSCrashMonitorType monitors = kscrash_setMonitoring(g_monitoring);
KSLOG_DEBUG("Installation complete.");
return monitors;
}
/** Called when a crash occurs.
*
* This function gets passed as a callback to a crash handler.
*/
static void onCrash(struct KSCrash_MonitorContext* monitorContext)
{
KSLOG_DEBUG("Updating application state to note crash.");
kscrashstate_notifyAppCrash();
monitorContext->consoleLogPath = g_shouldAddConsoleLogToReport ? g_consoleLogPath : NULL;
// 正在处理 crash 的时候,发生了再次 crash
if(monitorContext->crashedDuringCrashHandling)
{
kscrashreport_writeRecrashReport(monitorContext, g_lastCrashReportFilePath);
}
else
{
// 1. 先根据当前时间创建新的 crash 的文件路径
char crashReportFilePath[KSFU_MAX_PATH_LENGTH];
kscrs_getNextCrashReportPath(crashReportFilePath);
// 2. 将新生成的文件路径保存到 g_lastCrashReportFilePath
strncpy(g_lastCrashReportFilePath, crashReportFilePath, sizeof(g_lastCrashReportFilePath));
// 3. 将新生成的文件路径传入函数进行 crash 写入
kscrashreport_writeStandardReport(monitorContext, crashReportFilePath);
}
}
接下来的函数就是具体的日志写入文件的实现。2个函数做的事情相似,都是格式化为 json 形式并写入文件。区别在于 crash 写入时如果再次发生 crash, 则走简易版的写入逻辑 kscrashreport_writeRecrashReport()
,否则走标准的写入逻辑 kscrashreport_writeStandardReport()
。
bool ksfu_openBufferedWriter(KSBufferedWriter* writer, const char* const path, char* writeBuffer, int writeBufferLength)
{
writer->buffer = writeBuffer;
writer->bufferLength = writeBufferLength;
writer->position = 0;
/*
open() 的第二个参数描述的是文件操作的权限
#define O_RDONLY 0x0000 open for reading only
#define O_WRONLY 0x0001 open for writing only
#define O_RDWR 0x0002 open for reading and writing
#define O_ACCMODE 0x0003 mask for above mode
#define O_CREAT 0x0200 create if nonexistant
#define O_TRUNC 0x0400 truncate to zero length
#define O_EXCL 0x0800 error if already exists
0755:即用户具有读/写/执行权限,组用户和其它用户具有读写权限;
0644:即用户具有读写权限,组用户和其它用户具有只读权限;
成功则返回文件描述符,若出现则返回 -1
*/
writer->fd = open(path, O_RDWR | O_CREAT | O_EXCL, 0644);
if(writer->fd < 0)
{
KSLOG_ERROR("Could not open crash report file %s: %s", path, strerror(errno));
return false;
}
return true;
}
/**
* Write a standard crash report to a file.
*
* @param monitorContext Contextual information about the crash and environment.
* The caller must fill this out before passing it in.
*
* @param path The file to write to.
*/
void kscrashreport_writeStandardReport(const struct KSCrash_MonitorContext* const monitorContext,
const char* path)
{
KSLOG_INFO("Writing crash report to %s", path);
char writeBuffer[1024];
KSBufferedWriter bufferedWriter;
if(!ksfu_openBufferedWriter(&bufferedWriter, path, writeBuffer, sizeof(writeBuffer)))
{
return;
}
ksccd_freeze();
KSJSONEncodeContext jsonContext;
jsonContext.userData = &bufferedWriter;
KSCrashReportWriter concreteWriter;
KSCrashReportWriter* writer = &concreteWriter;
prepareReportWriter(writer, &jsonContext);
ksjson_beginEncode(getJsonContext(writer), true, addJSONData, &bufferedWriter);
writer->beginObject(writer, KSCrashField_Report);
{
writeReportInfo(writer,
KSCrashField_Report,
KSCrashReportType_Standard,
monitorContext->eventID,
monitorContext->System.processName);
ksfu_flushBufferedWriter(&bufferedWriter);
writeBinaryImages(writer, KSCrashField_BinaryImages);
ksfu_flushBufferedWriter(&bufferedWriter);
writeProcessState(writer, KSCrashField_ProcessState, monitorContext);
ksfu_flushBufferedWriter(&bufferedWriter);
writeSystemInfo(writer, KSCrashField_System, monitorContext);
ksfu_flushBufferedWriter(&bufferedWriter);
writer->beginObject(writer, KSCrashField_Crash);
{
writeError(writer, KSCrashField_Error, monitorContext);
ksfu_flushBufferedWriter(&bufferedWriter);
writeAllThreads(writer,
KSCrashField_Threads,
monitorContext,
g_introspectionRules.enabled);
ksfu_flushBufferedWriter(&bufferedWriter);
}
writer->endContainer(writer);
if(g_userInfoJSON != NULL)
{
addJSONElement(writer, KSCrashField_User, g_userInfoJSON, false);
ksfu_flushBufferedWriter(&bufferedWriter);
}
else
{
writer->beginObject(writer, KSCrashField_User);
}
if(g_userSectionWriteCallback != NULL)
{
ksfu_flushBufferedWriter(&bufferedWriter);
g_userSectionWriteCallback(writer);
}
writer->endContainer(writer);
ksfu_flushBufferedWriter(&bufferedWriter);
writeDebugInfo(writer, KSCrashField_Debug, monitorContext);
}
writer->endContainer(writer);
ksjson_endEncode(getJsonContext(writer));
ksfu_closeBufferedWriter(&bufferedWriter);
ksccd_unfreeze();
}
/** Write a minimal crash report to a file.
*
* @param monitorContext Contextual information about the crash and environment.
* The caller must fill this out before passing it in.
*
* @param path The file to write to.
*/
void kscrashreport_writeRecrashReport(const struct KSCrash_MonitorContext* const monitorContext,
const char* path)
{
char writeBuffer[1024];
KSBufferedWriter bufferedWriter;
static char tempPath[KSFU_MAX_PATH_LENGTH];
// 将传递过来的上份 crash report 文件名路径(/var/mobile/Containers/Data/Application/******/Library/Caches/KSCrash/Test/Reports/Test-report-******.json)修改为去掉 .json ,加上 .old 成为新的文件路径 /var/mobile/Containers/Data/Application/******/Library/Caches/KSCrash/Test/Reports/Test-report-******.old
strncpy(tempPath, path, sizeof(tempPath) - 10);
strncpy(tempPath + strlen(tempPath) - 5, ".old", 5);
KSLOG_INFO("Writing recrash report to %s", path);
if(rename(path, tempPath) < 0)
{
KSLOG_ERROR("Could not rename %s to %s: %s", path, tempPath, strerror(errno));
}
// 根据传入路径来打开内存写入需要的文件
if(!ksfu_openBufferedWriter(&bufferedWriter, path, writeBuffer, sizeof(writeBuffer)))
{
return;
}
ksccd_freeze();
// json 解析的 c 代码
KSJSONEncodeContext jsonContext;
jsonContext.userData = &bufferedWriter;
KSCrashReportWriter concreteWriter;
KSCrashReportWriter* writer = &concreteWriter;
prepareReportWriter(writer, &jsonContext);
ksjson_beginEncode(getJsonContext(writer), true, addJSONData, &bufferedWriter);
writer->beginObject(writer, KSCrashField_Report);
{
writeRecrash(writer, KSCrashField_RecrashReport, tempPath);
ksfu_flushBufferedWriter(&bufferedWriter);
if(remove(tempPath) < 0)
{
KSLOG_ERROR("Could not remove %s: %s", tempPath, strerror(errno));
}
writeReportInfo(writer,
KSCrashField_Report,
KSCrashReportType_Minimal,
monitorContext->eventID,
monitorContext->System.processName);
ksfu_flushBufferedWriter(&bufferedWriter);
writer->beginObject(writer, KSCrashField_Crash);
{
writeError(writer, KSCrashField_Error, monitorContext);
ksfu_flushBufferedWriter(&bufferedWriter);
int threadIndex = ksmc_indexOfThread(monitorContext->offendingMachineContext,
ksmc_getThreadFromContext(monitorContext->offendingMachineContext));
writeThread(writer,
KSCrashField_CrashedThread,
monitorContext,
monitorContext->offendingMachineContext,
threadIndex,
false);
ksfu_flushBufferedWriter(&bufferedWriter);
}
writer->endContainer(writer);
}
writer->endContainer(writer);
ksjson_endEncode(getJsonContext(writer));
ksfu_closeBufferedWriter(&bufferedWriter);
ksccd_unfreeze();
}
2.6.2 Crash 日志的读取逻辑
当前 App 在 Crash 之后,KSCrash 将数据保存到 App 沙盒目录下,App 下次启动后我们读取存储的 crash 文件,然后处理数据并上传。
App 启动后函数调用:
[KSCrashInstallation sendAllReportsWithCompletion:]
-> [KSCrash sendAllReportsWithCompletion:]
-> [KSCrash allReports]
-> [KSCrash reportWithIntID:]
->[KSCrash loadCrashReportJSONWithID:]
-> kscrs_readReport
在 sendAllReportsWithCompletion
里读取沙盒里的Crash 数据。
// 先通过读取文件夹,遍历文件夹内的文件数量来判断 crash 报告的个数
static int getReportCount()
{
int count = 0;
DIR* dir = opendir(g_reportsPath);
if(dir == NULL)
{
KSLOG_ERROR("Could not open directory %s", g_reportsPath);
goto done;
}
struct dirent* ent;
while((ent = readdir(dir)) != NULL)
{
if(getReportIDFromFilename(ent->d_name) > 0)
{
count++;
}
}
done:
if(dir != NULL)
{
closedir(dir);
}
return count;
}
// 通过 crash 文件个数、文件夹信息去遍历,一次获取到文件名(文件名的最后一部分就是 reportID),拿到 reportID 再去读取 crash 报告内的文件内容,写入数组
- (NSArray*) allReports
{
int reportCount = kscrash_getReportCount();
int64_t reportIDs[reportCount];
reportCount = kscrash_getReportIDs(reportIDs, reportCount);
NSMutableArray* reports = [NSMutableArray arrayWithCapacity:(NSUInteger)reportCount];
for(int i = 0; i < reportCount; i++)
{
NSDictionary* report = [self reportWithIntID:reportIDs[i]];
if(report != nil)
{
[reports addObject:report];
}
}
return reports;
}
// 根据 reportID 找到 crash 信息
- (NSDictionary*) reportWithIntID:(int64_t) reportID
{
NSData* jsonData = [self loadCrashReportJSONWithID:reportID];
if(jsonData == nil)
{
return nil;
}
NSError* error = nil;
NSMutableDictionary* crashReport = [KSJSONCodec decode:jsonData
options:KSJSONDecodeOptionIgnoreNullInArray |
KSJSONDecodeOptionIgnoreNullInObject |
KSJSONDecodeOptionKeepPartialObject
error:&error];
if(error != nil)
{
KSLOG_ERROR(@"Encountered error loading crash report %" PRIx64 ": %@", reportID, error);
}
if(crashReport == nil)
{
KSLOG_ERROR(@"Could not load crash report");
return nil;
}
[self doctorReport:crashReport];
return crashReport;
}
// reportID 读取 crash 内容并转换为 NSData 类型
- (NSData*) loadCrashReportJSONWithID:(int64_t) reportID
{
char* report = kscrash_readReport(reportID);
if(report != NULL)
{
return [NSData dataWithBytesNoCopy:report length:strlen(report) freeWhenDone:YES];
}
return nil;
}
// reportID 读取 crash 数据到 char 类型
char* kscrash_readReport(int64_t reportID)
{
if(reportID <= 0)
{
KSLOG_ERROR("Report ID was %" PRIx64, reportID);
return NULL;
}
char* rawReport = kscrs_readReport(reportID);
if(rawReport == NULL)
{
KSLOG_ERROR("Failed to load report ID %" PRIx64, reportID);
return NULL;
}
char* fixedReport = kscrf_fixupCrashReport(rawReport);
if(fixedReport == NULL)
{
KSLOG_ERROR("Failed to fixup report ID %" PRIx64, reportID);
}
free(rawReport);
return fixedReport;
}
// 多线程加锁,通过 reportID 执行 c 函数 getCrashReportPathByID,将路径设置到 path 上。然后执行 ksfu_readEntireFile 读取 crash 信息到 result
char* kscrs_readReport(int64_t reportID)
{
pthread_mutex_lock(&g_mutex);
char path[KSCRS_MAX_PATH_LENGTH];
getCrashReportPathByID(reportID, path);
char* result;
ksfu_readEntireFile(path, &result, NULL, 2000000);
pthread_mutex_unlock(&g_mutex);
return result;
}
int kscrash_getReportIDs(int64_t* reportIDs, int count)
{
return kscrs_getReportIDs(reportIDs, count);
}
int kscrs_getReportIDs(int64_t* reportIDs, int count)
{
pthread_mutex_lock(&g_mutex);
count = getReportIDs(reportIDs, count);
pthread_mutex_unlock(&g_mutex);
return count;
}
// 循环读取文件夹内容,根据 ent->d_name 调用 getReportIDFromFilename 函数,来获取 reportID,循环内部填充数组
static int getReportIDs(int64_t* reportIDs, int count)
{
int index = 0;
DIR* dir = opendir(g_reportsPath);
if(dir == NULL)
{
KSLOG_ERROR("Could not open directory %s", g_reportsPath);
goto done;
}
struct dirent* ent;
while((ent = readdir(dir)) != NULL && index < count)
{
int64_t reportID = getReportIDFromFilename(ent->d_name);
if(reportID > 0)
{
reportIDs[index++] = reportID;
}
}
qsort(reportIDs, (unsigned)count, sizeof(reportIDs[0]), compareInt64);
done:
if(dir != NULL)
{
closedir(dir);
}
return index;
}
// sprintf(参数1, 格式2) 函数将格式2的值返回到参数1上,然后执行 sscanf(参数1, 参数2, 参数3),函数将字符串参数1的内容,按照参数2的格式,写入到参数3上。crash 文件命名为 "App名称-report-reportID.json"
static int64_t getReportIDFromFilename(const char* filename)
{
char scanFormat[100];
sprintf(scanFormat, "%s-report-%%" PRIx64 ".json", g_appName);
int64_t reportID = 0;
sscanf(filename, scanFormat, &reportID);
return reportID;
}
2.7 前端 js 相关的 Crash 的监控
2.7.1 JavascriptCore 异常监控
这部分简单粗暴,直接通过 JSContext 对象的 exceptionHandler 属性来监控,比如下面的代码
jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
// 处理 jscore 相关的异常信息
};
2.7.2 h5 页面异常监控
当 h5 页面内的 Javascript 运行异常时会 window 对象会触发 ErrorEvent
接口的 error 事件,并执行 window.onerror()
。
window.onerror = function (msg, url, lineNumber, columnNumber, error) {
// 处理异常信息
};
2.7.3 React Native 异常监控
小实验:下图是写了一个 RN Demo 工程,在 Debug Text 控件上加了事件监听代码,内部人为触发 crash
<Text style={styles.sectionTitle} onPress={()=>{1+qw;}}>Debug</Text>
对比组1:
条件: iOS 项目 debug 模式。在 RN 端增加了异常处理的代码。
模拟器点击 command + d
调出面板,选择 Debug,打开 Chrome 浏览器, Mac 下快捷键 Command + Option + J
打开调试面板,就可以像调试 React 一样调试 RN 代码了。
查看到 crash stack 后点击可以跳转到 sourceMap 的地方。
Tips:RN 项目打 Release 包
-
在项目根目录下创建文件夹( release_iOS),作为资源的输出文件夹
-
在终端切换到工程目录,然后执行下面的代码
react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_iOS --sourcemap-output release_ios/index.ios.map;
-
将 release_iOS 文件夹内的
.jsbundle
和assets
文件夹内容拖入到 iOS 工程中即可
对比组2:
条件:iOS 项目 release 模式。在 RN 端不增加异常处理代码
操作:运行 iOS 工程,点击按钮模拟 crash
现象:iOS 项目奔溃。截图以及日志如下
2020-06-22 22:26:03.318 [info][tid:main][RCTRootView.m:294] Running application todos ({
initialProps = {
};
rootTag = 1;
})
2020-06-22 22:26:03.490 [info][tid:com.facebook.react.JavaScript] Running "todos" with {"rootTag":1,"initialProps":{}}
2020-06-22 22:27:38.673 [error][tid:com.facebook.react.JavaScript] ReferenceError: Can't find variable: qw
2020-06-22 22:27:38.675 [fatal][tid:com.facebook.react.ExceptionsManagerQueue] Unhandled JS Exception: ReferenceError: Can't find variable: qw
2020-06-22 22:27:38.691300+0800 todos[16790:314161] *** Terminating app due to uncaught exception 'RCTFatalException: Unhandled JS Exception: ReferenceError: Can't find variable: qw', reason: 'Unhandled JS Exception: ReferenceError: Can't find variable: qw, stack:
onPress@397:1821
<unknown>@203:3896
_performSideEffectsForTransition@210:9689
_performSideEffectsForTransition@(null):(null)
_receiveSignal@210:8425
_receiveSignal@(null):(null)
touchableHandleResponderRelease@210:5671
touchableHandleResponderRelease@(null):(null)
onResponderRelease@203:3006
b@97:1125
S@97:1268
w@97:1322
R@97:1617
M@97:2401
forEach@(null):(null)
U@97:2201
<unknown>@97:13818
Pe@97:90199
Re@97:13478
Ie@97:13664
receiveTouches@97:14448
value@27:3544
<unknown>@27:840
value@27:2798
value@27:812
value@(null):(null)
'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff23e3cf0e __exceptionPreprocess + 350
1 libobjc.A.dylib 0x00007fff50ba89b2 objc_exception_throw + 48
2 todos 0x00000001017b0510 RCTFormatError + 0
3 todos 0x000000010182d8ca -[RCTExceptionsManager reportFatal:stack:exceptionId:suppressRedBox:] + 503
4 todos 0x000000010182e34e -[RCTExceptionsManager reportException:] + 1658
5 CoreFoundation 0x00007fff23e43e8c __invoking___ + 140
6 CoreFoundation 0x00007fff23e41071 -[NSInvocation invoke] + 321
7 CoreFoundation 0x00007fff23e41344 -[NSInvocation invokeWithTarget:] + 68
8 todos 0x00000001017e07fa -[RCTModuleMethod invokeWithBridge:module:arguments:] + 578
9 todos 0x00000001017e2a84 _ZN8facebook5reactL11invokeInnerEP9RCTBridgeP13RCTModuleDatajRKN5folly7dynamicE + 246
10 todos 0x00000001017e280c ___ZN8facebook5react15RCTNativeModule6invokeEjON5folly7dynamicEi_block_invoke + 78
11 libdispatch.dylib 0x00000001025b5f11 _dispatch_call_block_and_release + 12
12 libdispatch.dylib 0x00000001025b6e8e _dispatch_client_callout + 8
13 libdispatch.dylib 0x00000001025bd6fd _dispatch_lane_serial_drain + 788
14 libdispatch.dylib 0x00000001025be28f _dispatch_lane_invoke + 422
15 libdispatch.dylib 0x00000001025c9b65 _dispatch_workloop_worker_thread + 719
16 libsystem_pthread.dylib 0x00007fff51c08a3d _pthread_wqthread + 290
17 libsystem_pthread.dylib 0x00007fff51c07b77 start_wqthread + 15
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
Tips:如何在 RN release 模式下调试(看到 js 侧的 console 信息)
- 在
AppDelegate.m
中引入#import <React/RCTLog.h>
- 在
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
中加入RCTSetLogThreshold(RCTLogLevelTrace);
对比组3:
条件:iOS 项目 release 模式。在 RN 端增加异常处理代码。
global.ErrorUtils.setGlobalHandler((e) => {
console.log(e);
let message = { name: e.name,
message: e.message,
stack: e.stack
};
axios.get('http://192.168.1.100:8888/test.php', {
params: { 'message': JSON.stringify(message) }
}).then(function (response) {
console.log(response)
}).catch(function (error) {
console.log(error)
});
}, true)
操作:运行 iOS 工程,点击按钮模拟 crash。
现象:iOS 项目不奔溃。日志信息如下,对比 bundle 包中的 js。
结论:
在 RN 项目中,如果发生了 crash 则会在 Native 侧有相应体现。如果 RN 侧写了 crash 捕获的代码,则 Native 侧不会奔溃。如果 RN 侧的 crash 没有捕获,则 Native 直接奔溃。
RN 项目写了 crash 监控,监控后将堆栈信息打印出来发现对应的 js 信息是经过 webpack 处理的,crash 分析难度很大。所以我们针对 RN 的 crash 需要在 RN 侧写监控代码,监控后需要上报,此外针对监控后的信息需要写专门的 crash 信息还原给你,也就是 sourceMap 解析。
2.7.3.1 js 逻辑错误
写过 RN 的人都知道在 DEBUG 模式下 js 代码有问题则会产生红屏,在 RELEASE 模式下则会白屏或者闪退,为了体验和质量把控需要做异常监控。
在看 RN 源码时候发现了 ErrorUtils
,看代码可以设置处理错误信息。
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
* @polyfill
*/
let _inGuard = 0;
type ErrorHandler = (error: mixed, isFatal: boolean) => void;
type Fn<Args, Return> = (...Args) => Return;
/**
* This is the error handler that is called when we encounter an exception
* when loading a module. This will report any errors encountered before
* ExceptionsManager is configured.
*/
let _globalHandler: ErrorHandler = function onError(
e: mixed,
isFatal: boolean,
) {
throw e;
};
/**
* The particular require runtime that we are using looks for a global
* `ErrorUtils` object and if it exists, then it requires modules with the
* error handler specified via ErrorUtils.setGlobalHandler by calling the
* require function with applyWithGuard. Since the require module is loaded
* before any of the modules, this ErrorUtils must be defined (and the handler
* set) globally before requiring anything.
*/
const ErrorUtils = {
setGlobalHandler(fun: ErrorHandler): void {
_globalHandler = fun;
},
getGlobalHandler(): ErrorHandler {
return _globalHandler;
},
reportError(error: mixed): void {
_globalHandler && _globalHandler(error, false);
},
reportFatalError(error: mixed): void {
// NOTE: This has an untyped call site in Metro.
_globalHandler && _globalHandler(error, true);
},
applyWithGuard<TArgs: $ReadOnlyArray<mixed>, TOut>(
fun: Fn<TArgs, TOut>,
context?: ?mixed,
args?: ?TArgs,
// Unused, but some code synced from www sets it to null.
unused_onError?: null,
// Some callers pass a name here, which we ignore.
unused_name?: ?string,
): ?TOut {
try {
_inGuard++;
// $FlowFixMe: TODO T48204745 (1) apply(context, null) is fine. (2) array -> rest array should work
return fun.apply(context, args);
} catch (e) {
ErrorUtils.reportError(e);
} finally {
_inGuard--;
}
return null;
},
applyWithGuardIfNeeded<TArgs: $ReadOnlyArray<mixed>, TOut>(
fun: Fn<TArgs, TOut>,
context?: ?mixed,
args?: ?TArgs,
): ?TOut {
if (ErrorUtils.inGuard()) {
// $FlowFixMe: TODO T48204745 (1) apply(context, null) is fine. (2) array -> rest array should work
return fun.apply(context, args);
} else {
ErrorUtils.applyWithGuard(fun, context, args);
}
return null;
},
inGuard(): boolean {
return !!_inGuard;
},
guard<TArgs: $ReadOnlyArray<mixed>, TOut>(
fun: Fn<TArgs, TOut>,
name?: ?string,
context?: ?mixed,
): ?(...TArgs) => ?TOut {
// TODO: (moti) T48204753 Make sure this warning is never hit and remove it - types
// should be sufficient.
if (typeof fun !== 'function') {
console.warn('A function must be passed to ErrorUtils.guard, got ', fun);
return null;
}
const guardName = name ?? fun.name ?? '<generated guard>';
function guarded(...args: TArgs): ?TOut {
return ErrorUtils.applyWithGuard(
fun,
context ?? this,
args,
null,
guardName,
);
}
return guarded;
},
};
global.ErrorUtils = ErrorUtils;
export type ErrorUtilsT = typeof ErrorUtils;
所以 RN 的异常可以使用 global.ErrorUtils
来设置错误处理。举个例子
global.ErrorUtils.setGlobalHandler(e => {
// e.name e.message e.stack
}, true);
2.7.3.2 组件问题
其实对于 RN 的 crash 处理还有个需要注意的就是 React Error Boundaries。详细资料
过去,组件内的 JavaScript 错误会导致 React 的内部状态被破坏,并且在下一次渲染时 产生 可能无法追踪的 错误。这些错误基本上是由较早的其他代码(非 React 组件代码)错误引起的,但 React 并没有提供一种在组件中优雅处理这些错误的方式,也无法从错误中恢复。
部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
它能捕获子组件生命周期函数中的异常,包括构造函数(constructor)和 render 函数
而不能捕获以下异常:
- Event handlers(事件处理函数)
- Asynchronous code(异步代码,如setTimeout、promise等)
- Server side rendering(服务端渲染)
- Errors thrown in the error boundary itself (rather than its children)(异常边界组件本身抛出的异常)
所以可以通过异常边界组件捕获组件生命周期内的所有异常然后渲染兜底组件 ,防止 App crash,提高用户体验。也可引导用户反馈问题,方便问题的排查和修复
至此 RN 的 crash 分为2种,分别是 js 逻辑错误、组件 js 错误,都已经被监控处理了。接下来就看看如何从工程化层面解决这些问题
2.7.4 RN Crash 还原
SourceMap 文件对于前端日志的解析至关重要,SourceMap 文件中各个参数和如何计算的步骤都在里面有写,可以查看这篇文章。
有了 SourceMap 文件,借助于 mozilla 的 source-map 项目,可以很好的还原 RN 的 crash 日志。
我写了个 NodeJS 脚本,代码如下
var fs = require('fs');
var sourceMap = require('source-map');
var arguments = process.argv.splice(2);
function parseJSError(aLine, aColumn) {
fs.readFile('./index.ios.map', 'utf8', function (err, data) {
const whatever = sourceMap.SourceMapConsumer.with(data, null, consumer => {
// 读取 crash 日志的行号、列号
let parseData = consumer.originalPositionFor({
line: parseInt(aLine),
column: parseInt(aColumn)
});
// 输出到控制台
console.log(parseData);
// 输出到文件中
fs.writeFileSync('./parsed.txt', JSON.stringify(parseData) + '\n', 'utf8', function(err) {
if(err) {
console.log(err);
}
});
});
});
}
var line = arguments[0];
var column = arguments[1];
parseJSError(line, column);
接下来做个实验,还是上述的 todos 项目。
-
在 Text 的点击事件上模拟 crash
<Text style={styles.sectionTitle} onPress={()=>{1+qw;}}>Debug</Text>
-
将 RN 项目打 bundle 包、产出 sourceMap 文件。执行命令,
react-native bundle --entry-file index.js --platform android --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_iOS --sourcemap-output release_ios/index.android.map;
因为高频使用,所以给 iterm2 增加 alias 别名设置,修改
.zshrc
文件alias RNRelease='react-native bundle --entry-file index.js --platform ios --dev false --bundle-output release_ios/main.jsbundle --assets-dest release_iOS --sourcemap-output release_ios/index.ios.map;' # RN 打 Release 包
-
将 js bundle 和图片资源拷贝到 Xcode 工程中
-
点击模拟 crash,将日志下面的行号和列号拷贝,在 Node 项目下,执行下面命令
node index.js 397 1822
-
拿脚本解析好的行号、列号、文件信息去和源代码文件比较,结果很正确。
2.7.5 SourceMap 解析系统设计
目的:通过平台可以将 RN 项目线上 crash 可以还原到具体的文件、代码行数、代码列数。可以看到具体的代码,可以看到 RN stack trace、提供源文件下载功能。
- 打包系统下管理的服务器:
- 生产环境下打包才生成 source map 文件
- 存储打包前的所有文件(install)
- 开发产品侧 RN 分析界面。点击收集到的 RN crash,在详情页可以看到具体的文件、代码行数、代码列数。可以看到具体的代码,可以看到 RN stack trace、Native stack trace。(具体技术实现上面讲过了)
- 由于 souece map 文件较大,RN 解析过长虽然不久,但是是对计算资源的消耗,所以需要设计高效读取方式
- SourceMap 在 iOS、Android 模式下不一样,所以 SoureceMap 存储需要区分 os。
文章内容过长,拆分为多个篇章,请自行点击查看,如果想整体连贯查看,请访问这里