通常我们去获取奔溃日志都是通过NSSetUncaughtExceptionHandler这个API得到NSException对象的信息。对于NSArray对象的数据越界是有用的,但是对于Array的数据越界是不会触发这个方法的。
NSArray和Array越界处理机制
NSArray
测试源码
1 | let arr = [1, 3, 4] |
NSArray越界奔溃线程调用栈
可以看出NSArray通过调用[NSArrayI objectAtIndexedSubscript:]方法获取到具体元素,在该方法调用完成后(pop rbp),触发了_CFThrowFormattedException方法,一个参数是NSExceptionName对象(NSRangeException), 另一个参数是异常原因(“*** %s: index %lu beyond bounds for empty array”),还有一个参数是调用方法(“-[NSArrayI objectAtIndexedSubscript:]”)。这三个参数会初始化一个NSException对象,当然当前线程的调用栈信息会赋值给这个NSException对象。这时就可以通过NSSetUncaughtExceptionHandler这个API的回调获取到这个NSException对象信息。
最后调用系统内核库的pthread kill方法,然后再执行act __set_ _astbsd()方法,最后exit()退出应用。
Array
测试源码
1 | let arr = [1, 3, 4] |
Array越界奔溃线程调用栈
可以看到Array越界的处理方式和NSArray完全不一样,最后是通过一个断言方法(Swift._fatalErrorMessage)让程序终止. “/BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang_Fall2018/swiftlang_Fall2018-1000.11.37.1/src/swift/stdlib/public/core/ContiguousArrayBuffer.swift”这个路径是Swift Array部分源码的路径,由于Swift开源的,可以到GitHub上clone下来看看源码。
可以看到Array源码获取元素前会先通过断言先判断数组是否会越界。但是我怎么知道这个断言方法被调用呢,或者这个断言触发了什么方法或指令让程序退出呢?
通过念茜姐姐的这个文章知道了有Mach异常还有Unix信号异常。既然断言不走NSSetUncaughtExceptionHandler,那么很有可能会通过给线程发送异常信号量来处理的。但是由于在debug环境下,Xcode会停在数组越界这行代码,不会再让你的程序往下走了,此时你的程序并没有跑完,可以将编译后的二进制文件在终端上运行(如下图)
得到运行结果如下:
得到一个错误的信息(“illegal hardware instruction”), 意思是非法指令,这就知道了,Swift的断言方法触发了非法指令。
非法指令对应的信号量
这时可以写这么一段代码来检测下了(当然还是在终端上运行编译后的二进制文件)
1 | signal(SIGILL) { (info) in |
运行结果如下:
bingo!
结论
- NSArray和Array越界的处理机制不同
- NSArray越界可以通过NSSetUncaughtExceptionHandler获取到程序奔溃的线程调用栈信息。
- Array越界通过断言来处理异常,而断言执行了非法指令,导致程序退出。
- 收集Array越界线程调用栈信息,可以通过signal(SIGILL, callBackHandle)方法