性能优化03:Crash

张建 lol

前言

在开发中最常见的就是 Crash,导致 Crash 的原因有各种各样,本文主要介绍 Crash

Crash 的类型

根据Crash 的不同来源,Crash 分为以下三类:

  • Mach 异常

最底层的 内核级异常。用户态的开发者可以直接通过 Mach API 设置thread,task,host的异常端口,来捕获Mach异常。

  • Unix 信号

又称 BSD 信号,如果开发者 没有捕获Mach异常,则会被 host层 的方法 ux_exception() 将异常转换为对应的 UNIX信号,并通过方法 threadsignal() 将信号投递到出错线程。可以通过方法 signal(x, SignalHandler) 来捕获signal。

  • NSException

应用级异常,它是未被捕获的 Objective-C 异常,导致程序向自身发送了 SIGABRT 信号而崩溃,是app自己可控的,对于未捕获的 Objective-C异常,是可以通过 try catch 来捕获的,或者通过 NSSetUncaughtExceptionHandler() 机制来捕获。

Mach相关知识

Mach内核作为系统一个底层的基础,仅与驱动操作系统所需的最低需要有关。 其他所有内容都由操作系统的更高层来实现,然后再利用Mach并以其认为合适的任何方式对其进行操作。

Mach提供了一小部分内核抽象,这些内核抽象被设计为既简单又强大。与Mach异常相关的内核抽象有:

  • tasks

资源所有权单位; 每个任务由一个虚拟地址空间、一个端口权限名称空间和一个或多个线程组成。 (类似于进程)

  • threads

任务中CPU执行的单位。

  • ports

安全的单工通信通道,只能通过发送和接收功能(称为端口权限)进行访问。

这些内核对象,对于Mach来说都是一个个的Object,这些Objects基于Mach实现自己的功能,并通过Mach Message来进行通信,Mach提供了相关的应用层的API来操作。与Mach异常相关的几个API有:

  • task_get_exception_ports:获取task的异常端口
  • task_set_exception_ports:设置task的异常端口
  • mach_port_allocate:创建调用者指定的端口权限类型
  • mach_port_insert_right:将指定的端口插入目标task

如何捕捉 Mach 异常

参考上图,主要的流程是:新建一个监控线程,在监控线程中监听 Mach 异常并处理异常信息。主要的步奏如下图:

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
static mach_port_t server_port;
static void *exc_handler(void *ignored);

//判断是否 Xcode 联调
bool ksdebug_isBeingTraced(void)
{
struct kinfo_proc procInfo;
size_t structSize = sizeof(procInfo);
int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};

if(sysctl(mib, sizeof(mib)/sizeof(*mib), &procInfo, &structSize, NULL, 0) != 0)
{
return false;
}

return (procInfo.kp_proc.p_flag & P_TRACED) != 0;
}

#define EXC_UNIX_BAD_SYSCALL 0x10000 /* SIGSYS */
#define EXC_UNIX_BAD_PIPE 0x10001 /* SIGPIPE */
#define EXC_UNIX_ABORT 0x10002 /* SIGABRT */
static int signalForMachException(exception_type_t exception, mach_exception_code_t code)
{
switch(exception)
{
case EXC_ARITHMETIC:
return SIGFPE;
case EXC_BAD_ACCESS:
return code == KERN_INVALID_ADDRESS ? SIGSEGV : SIGBUS;
case EXC_BAD_INSTRUCTION:
return SIGILL;
case EXC_BREAKPOINT:
return SIGTRAP;
case EXC_EMULATION:
return SIGEMT;
case EXC_SOFTWARE:
{
switch (code)
{
case EXC_UNIX_BAD_SYSCALL:
return SIGSYS;
case EXC_UNIX_BAD_PIPE:
return SIGPIPE;
case EXC_UNIX_ABORT:
return SIGABRT;
case EXC_SOFT_SIGNAL:
return SIGKILL;
}
break;
}
}
return 0;
}

static NSString *stringForMachException(exception_type_t exception) {
switch(exception)
{
case EXC_ARITHMETIC:
return @"EXC_ARITHMETIC";
case EXC_BAD_ACCESS:
return @"EXC_BAD_ACCESS";
case EXC_BAD_INSTRUCTION:
return @"EXC_BAD_INSTRUCTION";
case EXC_BREAKPOINT:
return @"EXC_BREAKPOINT";
case EXC_EMULATION:
return @"EXC_EMULATION";
case EXC_SOFTWARE:
{
return @"EXC_SOFTWARE";
break;
}
}
return 0;
}

void installExceptionHandler() {
if (ksdebug_isBeingTraced()) {
// 当前正在调试状态, 不启动 mach 监听
return ;
}
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
assert(kr == KERN_SUCCESS);

kern_return_t rc = 0;
exception_mask_t excMask = EXC_MASK_BAD_ACCESS |
EXC_MASK_BAD_INSTRUCTION |
EXC_MASK_ARITHMETIC |
EXC_MASK_SOFTWARE |
EXC_MASK_BREAKPOINT;

rc = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
if (rc != KERN_SUCCESS) {
fprintf(stderr, "------->Fail to allocate exception port\\\\\\\\n");
return;
}

rc = mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND);
if (rc != KERN_SUCCESS) {
fprintf(stderr, "-------->Fail to insert right");
return;
}

rc = thread_set_exception_ports(mach_thread_self(), excMask, server_port, EXCEPTION_DEFAULT, MACHINE_THREAD_STATE);
if (rc != KERN_SUCCESS) {
fprintf(stderr, "-------->Fail to set exception\\\\\\\\n");
return;
}

//建立监听线程
pthread_t thread;
pthread_create(&thread, NULL, exc_handler, NULL);
}

static void *exc_handler(void *ignored) {
// Exception handler – runs a message loop. Refactored into a standalone function
// so as to allow easy insertion into a thread (can be in same program or different)
mach_msg_return_t rc;
fprintf(stderr, "Exc handler listening\\\\\\\\n");
// The exception message, straight from mach/exc.defs (following MIG processing) // copied here for ease of reference.
typedef struct {
mach_msg_header_t Head;
/* start of the kernel processed data */
mach_msg_body_t msgh_body;
mach_msg_port_descriptor_t thread;
mach_msg_port_descriptor_t task;
/* end of the kernel processed data */
NDR_record_t NDR;
exception_type_t exception;
mach_msg_type_number_t codeCnt;
integer_t code[2];
int flavor;
mach_msg_type_number_t old_stateCnt;
natural_t old_state[144];
} Request;

Request exc;

struct rep_msg {
mach_msg_header_t Head;
NDR_record_t NDR;
kern_return_t RetCode;
} rep_msg;

for(;;) {
// Message Loop: Block indefinitely until we get a message, which has to be
// 这里会阻塞,直到接收到exception message,或者线程被中断。
// an exception message (nothing else arrives on an exception port)
rc = mach_msg( &exc.Head,
MACH_RCV_MSG|MACH_RCV_LARGE,
0,
sizeof(Request),
server_port, // Remember this was global – that's why.
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);

if(rc != MACH_MSG_SUCCESS) {
/*... */
break ;
};

//Mach Exception 类型
NSMutableString *crashInfo = [NSMutableString stringWithFormat:@"mach exception:%@ %@\n\n",stringForMachException(exc.exception), stringForSignal(signalForMachException(exc.exception, exc.code[0]))];

rep_msg.Head = exc.Head;
rep_msg.NDR = exc.NDR;
rep_msg.RetCode = KERN_FAILURE;

kern_return_t result;
if (rc == MACH_MSG_SUCCESS) {
result = mach_msg(&rep_msg.Head,
MACH_SEND_MSG,
sizeof (rep_msg),
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
}
//移除其他 Crash 监听, 防止死锁
NSSetUncaughtExceptionHandler(NULL);
signal(SIGHUP, SIG_DFL);
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
}

return NULL;
}

监听 Mach 异常需要注意:

  • 避免在 Xcode 联调时监听

原因是我们监听了EXC_BREAKPOINT这类型的Exception,一旦启动 app 联调后,会立即触发EXC_BREAKPOINT。而这段代码处理完后,会进入下一个循环等待,可主线程这是还等着消息处理结果,这就造成等待死锁。

关于代码中其他分析异常原因的代码,我会在下一篇讲获取堆栈的文章中详细解读。

Unix 信号(Signal)

Mach已经通过异常机制提供了底层的异常处理,但为了兼容更为流行的 POSIX 标准,BSD在Mach异常机制之上构建的 UNIX信号处理机制。异常信号首先被转换为Mach异常,如果没有被外界捕捉,则会被默认的异常处理 ux_exception() 转换为UNIX信号。

Unix 信号列表

Unix Signal 其实是由 Mach port 抛出的信号转化的,那么都有哪些信号呢?

  • SIGHUP

本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。

  • SIGINT

程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

  • SIGQUIT

和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

  • SIGABRT

调用abort函数生成的信号。
SIGABRT is a BSD signal sent by an application to itself when an NSException or obj_exception_throw is not caught.

  • SIGBUS

非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

  • SIGFPE

在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

  • SIGKILL

用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

  • SIGSEGV

试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

  • SIGPIPE

管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。

  • SIGSYS

非法的系统调用。

  • SIGTRAP

由断点指令或其它 trap 指令产生. 由d ebugger 使用。

  • SIGILL

执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

其他未列出的信号可以参照这篇文章:linux 各个SIG信号含义

如何捕捉 Unix 信号

一般来说我们需要捕捉以下信号:

1
2
3
4
5
6
7
8
9
10
11
static const int g_fatalSignals[] =
{
SIGABRT,
SIGBUS,
SIGFPE,
SIGILL,
SIGPIPE,
SIGSEGV,
SIGSYS,
SIGTRAP,
};

而要捕捉 Unix 信号,比 Mach 异常容易多了

实战:

AppDelegate.m中:

1
2
3
4
5
6
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
InstallSignalHandler();//信号量截断
InstallUncaughtExceptionHandler();//系统异常捕获
return YES;
}

SignalHandler.m的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void SignalExceptionHandler(int signal){

NSMutableString *mstr = [[NSMutableString alloc] init];

[mstr appendString:@"Stack:\n"];

void* callstack[128];

int i, frames = backtrace(callstack, 128);

char** strs = backtrace_symbols(callstack, frames);

for (i = 0; i
[mstr appendFormat:@"%s\n", strs[i]];
}
[SignalHandler saveCreash:mstr];
}

void InstallSignalHandler(void){

signal(SIGHUP, SignalExceptionHandler);

signal(SIGINT, SignalExceptionHandler);

signal(SIGQUIT, SignalExceptionHandler);

signal(SIGABRT, SignalExceptionHandler);

signal(SIGILL, SignalExceptionHandler);

signal(SIGSEGV, SignalExceptionHandler);

signal(SIGFPE, SignalExceptionHandler);

signal(SIGBUS, SignalExceptionHandler);

signal(SIGPIPE, SignalExceptionHandler);

}

UncaughtExceptionHandler.m的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void HandleException(NSException *exception){
// 异常的堆栈信息
NSArray *stackArray = [exception callStackSymbols];

// 出现异常的原因
NSString *reason = [exception reason];

// 异常名称
NSString *name = [exception name];
NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
NSLog(@"%@", exceptionInfo);
[UncaughtExceptionHandler saveCreash:exceptionInfo];
}

void InstallUncaughtExceptionHandler(void){
NSSetUncaughtExceptionHandler(&HandleException);
}

NSException

NSException 是应用级异常,是指 OC 代码运行过程由 Objective-C 抛出的异常,基本上是代码运行过程中的逻辑错误。比如往 NSArray 中插入 nil 对象,或者用 nil 初始化 NSURL 等。最简单区分一个异常是否 NSException 的方式是看这个异常能否被 @try catch finally 给捕获。

1
2
3
4
5
6
7
8
9
10
@try {
// 可能会出现崩溃的代码
}
@catch (NSException *exception) {
// 捕获到的异常exception
NSLog(@"捕获到的错误:%@", exception);
}
@finally {
// 结果处理
}

常见的 NSException 场景

  • 非主线程刷新UI

  • NSInvalidArgumentException

非法参数异常(NSInvalidArgumentException)是 Objective – C 代码最常出现的错误,所以平时在写代码的时候,需要多加注意,加强对参数的检查,避免 传入非法参数 导致异常,其中尤以nil参数为甚。

  • NSRangeException

越界异常(NSRangeException)也是比较常出现的异常。

  • NSGenericException

NSGenericException 这个异常最容易出现在foreach操作中,在 for in 循环中如果修改所遍历的数组,无论你是add或remove,都会出错 “for in”,它的内部遍历使用了类似 Iterator进行迭代遍历,一旦元素变动,之前的元素全部被失效,所以在foreach的循环当中,最好不要去进行元素的修改动作,若需要修改,循环改为for遍历,由于内部机制不同,不会产生修改后结果失效的问题。

  • NSInternalInconsistencyException

不一致导致出现的异常

比如 NSDictionary 当做 NSMutableDictionary 来使用,从他们内部的机理来说,就会产生一些错误:

1
2
NSMutableDictionary *info = method return to NSDictionary type;
[info setObject:@“sxm” forKey:@”name”];

比如xib界面使用或者约束设置不当

  • NSFileHandleOperationException

处理文件时的一些异常,最常见的还是存储空间不足的问题,比如应用频繁的保存文档,缓存资料或者处理比较大的数据:
所以在文件处理里,需要考虑到手机存储空间的问题。

  • NSMallocException

这也是内存不足的问题,无法分配足够的内存空间
此外还有

  • KVO Crash

移除未注册的观察者
重复移除观察者
添加了观察者但是没有实现-observeValueForKeyPath:ofObject:change:context:方法
添加移除keypath=nil
添加移除observer=nil

  • unrecognized selector send to instance

监听 NSException 异常

NSException的监听也十分简单:

1
2
3
4
5
6
7
8
void InstallUncaughtExceptionHandler(void) {
NSSetUncaughtExceptionHandler( &handleUncaughtException );
}

void handleUncaughtException(NSException *exception) {
NSString * crashInfo = [NSString stringWithFormat:@"yyyy Exception name:%@\nException reason:%@\nException stack:%@",[exception name], [exception reason], [exception callStackSymbols]];
NSLog(@"%@", crashInfo);
}

需要注意的是,在监听处理的方法中,是无法直接采集错误到堆栈的。详情我同样会在下一篇的崩溃堆栈收集的文章中介绍。

项目中:应用层面的 Crash

  1. NSInvalidArgumentException:非法参数
  • 原因:

    • 字典 :keyvaluenil
    • 数组:添加了 nil 元素
  • 解决方案:hook 相关方法,增加保护机制

  1. NSRangeException:下标越界
  • 原因:最常见的 数组 下标越界

  • 解决方案:hook 相关方法,增加保护机制

  1. unrecognized selector sent to instance
  • 原因:实例对象类对象 找不到 方法

  • 方法调用的过程:

    • 动态方法决议:resolveInstanceMethod/resolveClassMethod

      动态的添加方法:class_addMethod

    • 快速查找:forwardingTargetForSelector

      寻找别的对象接收消息

    • 慢速查找:

      methodSignatureForSelector(方法签名)+ forwardInvocation(修改方法调用对象)

    • 抛出异常:doesNotRecognizeSelector

  • 解决方案:

    • 方案一:hook 慢速查找两个方法,添加 try-catch
    • 方案二:hook 抛出异常 doesNotRecognizeSelector,添加 try-catch
  1. KVO
  • 原因:移除未注册的观察者、重复移除、添加观察者没有实现、添加移除keypath/observer为nil

  • 解决方案:添加移除 成对出现

  1. 多线程中的崩溃
  • 原因:子线程中更新UI

  • 解决方案:主线程更新UI

项目中:Signal 层面的crash

除了 OC 层面的 crash 之外,还有 内存错误、访问不存在的内存地址等 产生的 crash,则需要利用 unix 标准的 signal 机制,注册 SIGABRT、SIGBUS、SIGSEGV 等信号发生时的处理函数。

  • SIGKILL:用来立即结束程序的运行的信号。
  • SIGSEGV:试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
  • SIGABRT:调用abort函数生成的信号。
  • SIGTRAP:由断点指令或其它trap指令产生。
  • SIGBUS:非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

检测方式

  1. 第三方框架:Bugly

  2. iTunes Store收集:上传App Store的app,苹果有帮我们收集,Xcode->Windows->Organizer -> Crashs

  3. NSSetUncaughtExceptionHandler:iOS SDK 中提供了一个现成的函数

什么是符号表

Xcodetarget 进行 编译、归档,生成 .xcarchive 文件,这个 .xcarchive 文件包含了 应用、符号表信息以及其他的资源,而 符号表.dSYM 文件就是 符号表,其包含了 一个16进制保存的函数地址映射信息(包括文件名、函数名、行号等),所以称之为 调试符号信息文件

符号表有什么作用?

符号表 就是用来 符号化 crash log(崩溃日志),通过 符号化 能够 还原 符号表中的 crash 堆栈信息,最直观的体现就是能够看到 方法名

  • Post title:性能优化03:Crash
  • Post author:张建
  • Create time:2023-03-09 05:32:04
  • Post link:https://redefine.ohevan.com/2023/03/09/OC性能优化/性能优化03:Crash/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.