文章目录
本文对iOS应用上经常遇到的Crash(常规signal触发的闪退)进行分析,并结合笔者的优化经验,给出Crash分析和建议。
一、Crash与信号
1.1 Crash是如何产生的
在iOS上Crash最终都是通过signal的形式发送给应用,应用可以通过注册signal handler来选择处理或忽略大部分信号。Crash的原因有多种,系统通过不同的signal来告知app这大概是什么原因造成的,常见的信号比如SIGSEGV,SIGTRAP,SIGABRT。
1.1.1 signal
signal是一个4字节的整形数字,在iOS/OSX中定义了31个已知的信号;
crash仅仅是singal触发的一个行为。signal的用途/产生包括但不限于:
- 显式调用kill,killpg触发signal
- 改变子进程的状态
- 致命性中断
- job控制
- timer过期
- 各种通知,如cpu resource limit或file size limit等
signal会导致以下几种行为(action):
- Terminate 杀死进程
- Dump core 杀死进程并创建一个core file
- Stop 暂停进程
- Continue 恢复进程
- Ignore 忽略/丢弃该信号
每个signal号都有默认的action,可以通过sigaction() 系统调用来修改signal actions,为SIG_DFL(use the default action),或者修改为SIG_IGN(ignore the signal),或者指定signal handler function(捕获signal)
常见信号与原因如下:
信号 | 值 | 官方注释 | 可能原因 |
---|---|---|---|
SIGILL | 4 | illegal instruction (not reset when caught) | ILL_ILLTRP at 0xxxx通常是二进制出错,典型比如app升级前后或者二进制缓存出错 |
SIGTRAP | 5 | trace trap (not reset when caught) | __builtin_trap()系统调用brk触发软中断结束进程,一般是数据或参数校验异常 |
SIGABRT | 6 | abort() | 调用abort(),比如典型的NS异常或C++异常 |
SIGKILL | 9 | kill (cannot be caught or ignored) | 系统升级,app升级,0x8badf00d,Jetsam,XCode调试杀死app,摄像头权限变更等 |
SIGBUS | 10 | bus error | 总线错误,内存访问未对齐 |
SIGSEGV | 11 | segmentation violation | 内存访问越界,内存crash或者地址错误,如栈溢出等 |
SIGPIPE | 13 | write on a pipe with no one to read it | 管道异常,socket通信异常 |
SIGSTOP | 17 | sendable stop signal not from tty | XCode调试时pause操作可触发 |
其中SIGKILL,SIGSTOP信号是无法被捕获或忽略等自定义处理的;
1.1.2 signal处理流程
signal的一般流程大概如下:
我们可以简单的使用kill函数来向应用/线程发送信号:
int kill(pid_t pid, int sig);
int pthread_kill(pthread_t thread, int sig);
1.1.3 signal捕获
一般的signal异常捕获工具会基于上述方式来修改signal的actions或执行signal handler,从而达到异常捕获的目的。我们也可以通过修改signal actions来达到让应用直接忽略某个信号,而不发生异常退出。但是这也有个别例外情况,比如SIGKILL,SIGSTOP 无法被捕获或忽略。如下:
struct sigaction action = {
{0}};
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++)
{
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;
}
}
static void handleSignal(int sigNum, siginfo_t* signalInfo, void* userContext)
{
KSLOG_DEBUG("Trapped signal %d", sigNum);
//...异常捕获
}
以及
KSLOG_DEBUG(@"Backing up original handler.");
g_previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
KSLOG_DEBUG(@"Setting new handler.");
NSSetUncaughtExceptionHandler(&handleException);
1.2 Crash日志简析
当发生Crash时我们需要尽可能的收集更多的信息,包括crash的堆栈信息以及crash前的一些用户操作记录或者等其它附加信息。
Crash日志就是我们处理Crash时最重要的信息,它记录了一个app是因为什么原因发生了闪退,我们可以根据这些信息去进行分析。
一个Crash日志大概如下:
{"app_name":"mttlite","timestamp":"2019-03-26 13:04:38.95 +0800","app_version":"9.2.0","slice_uuid":"9c96c0a4-9019-39ea-992b-da986ae8cc2d","adam_id":0,"build_version":"9.2.0.8735","bundleID":"com.tencent.mttlite","share_with_app_devs":false,"is_first_party":false,"bug_type":"109","os_version":"iPhone OS 12.0 (16A366)","incident_id":"733801EE-A7D3-44E7-9D12-06BED94B4FAC","name":"mttlite"}
Incident Identifier: 733801EE-A7D3-44E7-9D12-06BED94B4FAC
CrashReporter Key: f4d479f63324962b4280137a80389c1f3f46b019
Hardware Model: iPhone11,2
Process: mttlite [637]
Path: