WWDC 2018 Session 414: Understanding Crashes and Crash Logshtml
查看更多 WWDC 18 相关文章请前往 老司机x知识小集xSwiftGG WWDC 18 专题目录ios
做者简介:@Vong,目前就任于美拍,喜欢折腾~git
人非圣贤,孰能无过。每一个人在写代码的时候,或多或少都会犯错,那么如何调试、找出问题所在呢?让咱们跟随苹果工程师一块儿了解一下崩溃是如何产生以及如何解决它们的吧。github
崩溃是什么?崩溃是当应用想要作某件事的时候,被意外终止。macos
主要是如下几方面缘由编程
NSArray
或者 Swift.Array
越界当咱们链接着 Xcode
进行调试的时候,遇到崩溃,大概长这个样子。 数组
当连着调试器的时候,咱们可以拿到崩溃现场的一些调用栈以及对应的方法,当没有连着调试器的时候,系统会将崩溃日志存储到磁盘当中。markdown
一般状况下,release
模式的应用的崩溃日志是没有符号化的,日志内记录的都是地址。咱们能够经过 Xcode
来将崩溃日志进行符号化,解析出对应文件名、方法名以及对应崩溃在第几行。多线程
获取崩溃日志的方式不少,咱们先来了解一下如何经过 Xcode Organizer
来获取从 TestFlight
或App Store
下载的应用的崩溃日志。app
先来看一下下面这张图:
下面数字 1~6 分别表明图中标注的 1~6
App Store
或者 TestFlight
上的应用。PS:上面6个只是简单介绍了一下主题部分,剩余的能够自行探索使用。好比搜索、对单个日志作一些笔记、以及将已修复的崩溃标记为已解决等等。
那么如何才能在 Organizer
中获取对应的崩溃日志呢?很简单,只须要作到下面几步
Xcode
中登陆已付费的开发者账号。App Store
或 TestFlight
时,一并上传符号文件。Xcode Organizer
窗口,选中 Crashes
tab(快捷键:Cmd+Shift+6
)。链接上设备,打开 Xcode
,使用快捷键 Cmd+Shift+2
来打开 Devices Window
,选中对应设备,而后选择 View Device Logs
,便可查看当前设备磁盘上的全部崩溃文件,找到应用对应的日志便可展开分析。
有些时候,获取到的崩溃日志并无符号化。这个时候须要本身作一些额外操做,这里能够参考我以前在知识小集分享过的一个小 tip——iOS快速解析崩溃日志。
Xcode
的自动化测试(获得的是已符号化的日志)Mac
自带的 Console
应用,获取 Mac
或者模拟器的崩溃日志Xcode Organizer
的 Crashes
tab 中呈现。Xcode
会自动进行符号化。Xcode Organizer
的 Archive
tab 为已开启 bitcode
的应用下载 dSYM
文件。MacOS
上可见)首先从崩溃缘由中的崩溃类型开始
如上图的崩溃类型为 EXC_BAD_INSTRUCTION
,它表明 CPU
尝试在执行一段不存在或无效的代码,而致使进行被“杀死”。
而后咱们能够找到崩溃线程的调用栈的前几行,结合崩溃信息(若是有的话)进一步分析。找到崩溃栈中第一处二进制名为应用名称所在那一行,进到对应文件对应的代码行数进行查看(如上图中标红的那一行),而后进一步分析。上图中的崩溃能够很明显看出其缘由是对 nil
进行了强制解包。
断言和先决条件的意义在于当错误发生时,强制终止当前进程。
上述提到的对 nil
强制解包致使的崩溃是断言和先决条件中的一种。而它们还包含下面几种状况:
某些状况下,系统处于保护目的,会将一些异常的应用“杀死”。如下几种场景可能触发系统将应用“杀死”:
以上几种场景致使的崩溃,其崩溃日志能够在上面提到的 Device Window
中查看,Organizer Window
并不必定可以收集到这些日志。更多细节能够参考苹果的这个技术讲座 Understanding and Analyzing Application Crash Reports。
先来看一个关于看门狗的例子。
上面的崩溃类型为 EXC_CRASH (SIGKILL)
,SIGKILL
通常表明的是系统终止了进程的运行,这种信号没法被应用捕获,进而也就没法处理。终止缘由为 Namespace SPRINGBOARD, Code 0x8badf00d
,若是你有查看上面提到的关于崩溃日志的讲座,你应该会知道 Code 0x8badf00d
表明什么。从终止描述中来看,是因为启动时长超过了 19.97 秒。
此次总算知道为何看门狗对应的
code
是0x8badf00d
了,从此次苹果工程师的发音上来看,这个code
的发音同ate bad food
。
应用审核被拒的比较常见的缘由就包含启动超时这一项。那么如何来避免这种状况发生呢?苹果工程师给了咱们这些建议:
常见的内存错误包含:过分释放、野指针(访问已释放对象)、内存访问越界(好比 C 数组)。咱们仍是经过一个日志来分析一下具体问题。
由上图中标注的1,咱们知道崩溃类型为 EXC_BAD_ACCESS(SIGSEGV)
,这种类型崩溃主要是有两种状况致使:
经过崩溃栈中的objc_release
、object_dispose
等,咱们更加肯定这是因为内存问题致使的崩溃。咱们经过这几个线索能够知道,LoginViewController
实例在调用 deinit
方法销毁相关属性的时候,发生了内存问题,进而致使崩溃的产生。
咱们回到日志的第一部分中的Exception Codes
,苹果的工程师说能够根据经验以及日志中的相关信息得出结论,对应的 BAD_ADDRESS
为 0x7fdd5e70700
。缘由是 0x7fdd5e70700
恰好在日志中的这一段 MALLOC_TINY 00007fdd5e400000-00007fdd5e800000
地址范围内。
一些关于内存及释放的基础
Objective-C
对象以及一些 Swift
对象的内存布局如图,当一个对象有效(未释放)时以 isa
开始,isa
指向它所属的类。objc_release
主要是读取对象的 isa
指针,而后将 isa
指针解除对 Class
的引用。
正常状况下,一切都能照常工做。若是对象已经被释放,会发生什么呢?free
函数调用后,会将对象删除,而且将其插入到包含了其它已释放对象组成的链表中,同时将以前 isa
区域指向链表中下一个已释放对象。
当以前的 isa
内存区域被写入成 rotated free list
指针时,意味着访问这个地址返回的将是一个无效的内存地址,进而致使崩溃。因此当 objc_release
去解除 isa
引用时,访问到的是 rotated free list
,因此崩溃就发生了。
因此能够分析出,确定是在释放某个属性时,该属性已经被释放。咱们能知道具体是哪一个属性致使的么?答案是确定的。
目前从崩溃的那一行来看,__ivar_destroyer
是编译器帮咱们自动生成的函数,因此咱们无从知晓具体是哪一行致使的问题。咱们只知道这个类有如图三个属性:
可是从 @objc LoginViewController.__ivar_destroyer + 42
能够获取到一些信息,+42
表明着汇编里面的该函数的偏移量。咱们能够对 __ivar_destroyer
函数进行反汇编,而后看偏移量为42对应获取的是哪一个属性,在 Xcode
中可使用 lldb
调试。
断点后分别输入上图中黄色字的命令,分别为 command script import lldb.macosx.crashlog
,crashlog /Users/.../RideSharingApp-2018-05-24-1.crash
,后面的路径须要替换成你的崩溃日志路径。Xcode
会自动检索二进制文件以及对应的 dSYM
文件,而后符号化显示在 lldb
控制台中。而后咱们找到崩溃处的地址,执行以下命令,便可获得对应的反汇编代码:
咱们不须要理解每一行汇编的意思,每行后面的注释能够帮助咱们理解,根据注释能够知道 一、二、3 处代码分别表明着 userName
、database
、views
的释放。回到上面提到的 +42
,咱们找到第3处的第一行,有一点须要注意的是大部分状况下汇编的偏移地址是返回地址,因此调用 objc_release
是在上一行。因此能够判断出是在释放 database
时出现了问题。虽然咱们目前还不知道具体问题所在,可是能够经过这些信息缩小查找问题的范围,能够查找使用到 database
的地方,来找到真正的问题所在。
bad address
问题objc_msgSend 或者 retain/release 崩溃
没法识别的方法异常
abort() inside malloc/free
bug
出现的缘由Xcode
提供的工具来复现内存问题,好比 Address Sanitizer
或者 Zombies
多线程问题即便咱们拿到日志大几率状况下也没法分析问题所在,即便连着 Xcode
调试也不必定可以稳定复现,即便运气好能复现也可能分析不出具体问题。因此咱们能够借助 Xcode
提供的工具来帮咱们分析,这个工具就是 Thread Sanitizer
。经过快捷键 Cmd+shift+,
,而后选则 Diagnostics
tab,勾选 Thread Sanitizer
便可。以下图所示
在建立 GCD Queue
、(NS)OperationQueue
、(NS)Thread
时,使用自定义名称,方便后续调试以及崩溃日志内查看。
let queue = DispatchQueue(label: "com.example.myapp.networking") let operationQueue = OperationQueue() operationQueue.name = "Networking OperationQueue" let thread = Thread(...) thread.name = "Networking Thread" 复制代码
Address Sanitizer
来查看内存问题Thread Sanitizer
来查看多线程问题