[译]理解 iOS 异常类型 <🌟>
最近正在研究crash相关处理的方法,看到一篇翻译的文章,讲述关于objective c异常类型,这里记录下,供学习参考。
转载于:https://juejin.im/post/6844903866128039944
当你的iOS应用崩溃的时候,我们需要去分析异常日志以定位根本原因。崩溃可能是 “低内存崩溃 Low Memory Crash” 或者 “普通异常崩溃”。当碰到“异常”时,更好的理解“不同类型的异常”能够真正帮助我们快速定位问题所在。
在这篇文章中,我们将研究 iOS 应用可能碰到的不同类型的“异常”,例如EXC_CRASH、EXC_BAD_ACCESS、EXC_RESOURCE、00000020 等。
崩溃日志中的“异常”
“异常”这个词在“崩溃日志”语境下更多与“Mach 异常”(以“EXC_*为前缀”)和 “*UNIX 信号”(如: SIGSEGV, SIGBUS等)相关。在某些情况下(应该是有对应的dSYM符号文件时)系统会通过映射将底层的 Mach 异常 翻译为 UNIX 信号。这就是为什么你能log中看到有用 “EXC_CRASH(SIGABRT)” 及 “EXC_BAC_ACCESS(SIGSEGV)” 作为 异常类型(Exception Type)
。
对于某些异常,还会附带一个关联的 处理器定制异常码(processor-specific Exception Code) 或者 异常子类型(Exception Subtype),用以包含更多问题相关信息。举例来说, “EXC_BAC_ACCESS” 类型异常可能有一行如“KERN_INVALID_ADDRESS at 0x80000010”作为“异常码”; “EXC_RESOURCE” 可能有一行"WAKEUPS"作为"异常子类别"。
UNIX 信号
iOS开发者常见的 UNIX 信号 如下:
UNIX 信号 | 注释 |
---|---|
SIGSEGV | 访问无效的内存地址。地址存在,但是应用程序无法访问。 |
SIGABRT | 程序崩溃。由 C函数 abort() 初始化。通常意味着系统检测到某些事务出错,例如 assert() 或者 NSAssert() 校验失败。 |
SIGBUS | 访问无效的内存地址。地址不存在,或对齐无效。(The address does not exist, or the alignment is invalid.) |
SIGTRAP | 调试器相关 |
SIGILL | 尝试执行非法的、有缺陷、未知的或者需要权限的指令。 |
更多 UNIX 信号 可以参考这里:Unix_signal。
Mach 异常
Mach 异常 | 描述 | 注释 |
---|---|---|
EXC_BAD_ACCESS | 错误内存访问 | 访问“错误”内存地址。“错误”可能指“地址不存在”或者“应用没有权限访问”。因此通常与 SIGBUS 及 SIGSEGV 相关联。 |
EXC_CRASH | 异常跳出 | 通常与 SIGABRT 相关联,意思是由于检测到代码抛出的未捕获异常而使应用程序异常退出。 |
EXC_BREAKPOINT | 跟踪/断点捕获 | 通用与 SIGTRAP 相关联。可以由你自己的代码或者 NSExceptions 抛出时触发。 |
EXC_GUARD | 违反了受保护资源的防护(Violated Guarded Resource Protection) | 由违背受保护资源防护触发,例如‘某些文件描述符’。 |
EXC_BAD_INSTRUCTION | 非法指令 | 通常与特定非法或未定义指令/操作数相关。 |
EXC_RESOURCE | 资源限制 | 应用由于达到资源消耗限制而退出。 |
00000020 | 十六进制异常类型 | 非 'OS Kernel' 异常。 |
查看完整 Mach 异常列表请参考 这里 (
sys/osfmk/mach/exception_types.h
)的源码文件。
异常
EXC_BAD_ACCESS(错误内存访问)
“EXC_BAD_ACCESS” 是APP崩溃时最常见的异常之一。不幸的是,调试起来却不容易。
一般有两种可能性:
- 访问某些尚未初始化的对象。(SIGBUS)
- 访问已经被 ARC 释放(导致地址变为不可访问)的对象。如果是这个情况,你通常可以在崩溃日志中的 “Backtrace” 顶部附近看到
objc_release
。
示例如:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x6d783f44
...
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Codes: KERN_PROTECTION_FAILURE at 0x00000011
“EXC_BAD_ACCESS”也有关联的“异常码”以帮助提供额外信息。举例来说,KERN_PROTECTION_FAILURE
表示内存有效,但是不允许当前形式的访问,KERN_INVALID_ADDRESS
意思是地址当前无效。
查看这里的源码文件获取完整的可能值列表。
为了辅助调试 “EXC_BAD_ACCESS” 类型异常,你可以勾选 Xcode 中的 “Enable Zombie Objects” 后再尝试。
EXC_CRASH(异常跳出)
相较于 “EXC_BAD_ACCESS”,“EXC_CRASH" 更容易遇到。它通常发生在对象接收到未实现的消息时,如 Xcode 调试器中显示的 “unrecognized selector sent to instance 0x6a33840”。
一般情况里这个异常会与调试器一起发挥作用,因为调试器可以中断进程。如果没有附加调试器,会生成一个崩溃日志。
崩溃日志中展示的信息示例:
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
...
## Usually you will see a similar line in the "backtrace" part
2 CoreFoundation 0x36c02e02 -[NSObject(NSObject) doesNotRecognizeSelector:] + 166
可能存在某些与“unrecognized selector”无关的特殊情况。如果碰到了,请注意到处都有可能发生这种事情。
另一个常见的“EXC_CRASH”情况是关于“应用扩展(App Extensions)”。应用扩展如果“花了太长时间来初始化”则会被系统终止。在这种情况下,异常子类型(Exception Subtype)*显示为 LAUNCH_HANG
,附带一个得体的*异常消息(Exception Message):
Exception Type: EXC_CRASH (SIGABRT)
Exception Subtype: LAUNCH_HANG
Exception Message: The extension took too much time to initialize
EXC_BREAKPOINT(跟踪捕获)
与“EXC_CRASH”非常相似,EXC_BREAKPOINT 也往往与调试器一起发挥作用,在测试阶段被捕获。 当使用 Swift 时,在以下情况这个异常会在运行时抛出:
- 一个非可选类型值为nil
- 强制类型转换失败
示例信息如:
Exception Type: EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000002, 0x0000000000000000
复制代码
你可以在代码中手动调用
__builtin_trap()
来触发这个异常。
EXC_GUARD(违反了受保护资源的防护)
与其他所有“EXC_”前缀的异常不同,这个异常不是一个“原生”的 Mach 异常。事实上,它是为 XNU - 一个苹果开发的衍生操作系统内核 而添加的。
"XNU" 代表 "X 不是 Unix"(X is Not Unix)。 “EXC_GUARD”的定义可以在这里-
osfmk/mach/exception_types.h
找到。
这个异常的一个较好例子是应用程序在 Core Data 访问 SQLite 文件时关闭了它的“文件描述符(file descriptor)”。
在 iOS7 之前,这个异常会附带一部分“异常码(Exception Codes)”以帮助理解情况。异常码包含“两个”位域代码(如:0x400000010000005e
)及subcode(如:0x00007f8254a019c0
)。
位域代码部分可分解为“3”个区:
- Guard Type - 这个时候只有一种类型 - 受保护的文件描述符(guarded file descriptor(GUARD_TYPE_FD))。值为
0x2
。如果你看到时 0x4 作为代码的前缀,则这个崩溃与“文件描述符”相关。 - Flavor - 当违反“受保护的文件描述符”时的不同条件: 如果设置了“第1”(
[32]: "1 << 0"
)位(kGUARD_EXC_CLOSE),则它曾试图在“受保护的文件描述”上调用close()
。 如果设置了“第2”([33]: "1 << 1"
)位(kGUARD_EXC_DUP),则它试图在“受保护的文件描述符”上使用F_DUPFD
或F_DUPFD_CLOEXEC
调用dup(2)
,dup2(2)
,fcntl(2)
。还包含了尝试使用/dev/fd/
打开“文件描述符”。 如果设置了“第3”([34]: "1 << 2"
)位(kGUARD_EXC_NOCLOEXEC),则它试图关闭“文件描述符”上的“close-on-exec”标志。 如果设置了“第4”([35]: "1 << 3"
)位(kGUARD_EXC_SOCKET_IPC),则它试图通过 套接字(socket)发送“受保护的文件描述符”。 如果设置了“第5”([36]: "1 << 4"
)位(GUARD_FILEPORT),则它曾试图通过 套接字(socket)从“受保护的文件描述符”创建一个文件端口。 如果设置了“第6”([37]: "1 << 5"
)位(kGUARD_EXC_MISMATCH),说明“受保护的文件描述符”与“守卫”不相符。 如果设置了“第7”([38]: << 6
)位(kGUARD_EXC_WRITE),则它曾试图通过 套接字(socket)写入一个“受保护的文件描述符”。 - File Descriptor - 应用尝试操作的受保护的文件描述符。- subcode部分包含“受保护的值”。
详细定义可以在这里(
/bsd/sys/guarded.h
)找到。
从 iOS 7 开始,“Exception Codes”被提供更详细解释的“Exception Subtype”和“Exception Message”替代。
# iOS 6
Exception Type: EXC_GUARD
Exception Codes: 0x400000010000005e, 0x00007f8254a019c0
# The type is "GUARD_TYPE_FD" (0x4), with "kGUARD_EXC_CLOSE". The FD is "94".
# -------
# iOS 7 and above
Exception Type: EXC_GUARD
Exception Subtype: GUARD_TYPE_FD
Exception Message: CLOSE on file descriptor 81 (guarded with 0x0000000017e6eed0)
EXC_BAD_INSTRUCTION(非法指令)
“EXC_BAD_INSTRUCTION”,通常与“SIGILL”关联,是一个非常容易理解的异常 - 即你正在使用“错误”的指令或操作。然而,有时候也很难去调试。
以下是一些较常见的情况。 由于Xcode提供的调试信息,这个很容易识别 - 它是由于不安全的解包导致的。
## Usually show "EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)" in Xcode.
"fatal error: unexpectedly found nil while unwrapping an Optional value"
但是,像这样和这样(均为StackOverflow上的问题)的就不容易了 - 第一个是有关于 GCD 的使用,另一个是苹果的bug。 以下是崩溃日志中的显示格式:
Exception Type: EXC_BAD_INSTRUCTION (SIGILL)
Exception Codes: 0x0000000000000001, 0x000000000000b6d2
EXC_RESOURCE
“EXC_RESOURCE”意思是进程“达到资源消耗上限”。通常,当你的应用在一定时间内持续超出限制时会被触发。 这个异常包含“Exception Subtype”以帮助理解实际情况:
- CPU - 限制为
50%
,时间不超过180秒
。 - WAKEUPS - 表示线程每秒唤醒次数太多。限制为
150次/每秒
, 时间不超过300秒
。 - MEMORY - 没有相关文档描述限制信息。
与“EXC_GUARD”类似,它曾使用“位域”来传递信息,现在也使用“Exception SubType”和“Exception Message”。
Exception Type: EXC_RESOURCE
Exception Subtype: CPU
Exception Message: (Limit 50%) Observed 85% over 180 secs
---
Exception Type: EXC_RESOURCE
Exception Subtype: WAKEUPS
Exception Message: (Limit 150/sec) Observed 206/sec over 300 secs
---
Exception Type: EXC_RESOURCE
Exception Subtype: MEMORY
Exception Message: Crossed High Water Mark
00000020
与“EXC_”异常不同,这个“异常类型”实际上不能告诉你任何信息。取而代之,你应该查看“异常代码”获取更多详情。
0x8badf00d
(读作 ate bad food)- 表示由于 watchdog 出现超时而导致应用被操作系统终止。通常意味着应用程序花了太长时间启动、关闭或者响应系统事件。一个非常典型的情况是“在主线程上做同步网络请求”。0xbaaaaaad
(读作 “plooookhy”)- 表示日志是整个系统的堆栈,而不是崩溃报告。0xc00010ff
(读作 cool off(冷静))- 表示应用程序被系统关闭以响应热事件。0xbad22222
- 表示操作系统终止了一个VoIP程序,因为它过于频繁的执行恢复。0xdead10cc
(读作 dead lock(死锁))- 表示应用在后台运行时保持了系统资源。0xdeadfa11
(读作 deadfall)- 表示应用被用户强制关闭了。强制关闭发生于用户先按下电源键直到“滑动来关机”出现然后按住主屏幕按钮。
这些“十六进制”代码实际上是六音词 - 由我们开发者创建作为不容易忘记的魔法数字。
扩展阅读
你可查看这篇文章 Demystifying iOS Application Crash Logs 以了解iOS异常日志结构。