XCode4.0以后,LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。简单的说,编译器有两个职责:把 Objective-C 代码转化成低级代码,以及对代码做分析,确保代码中没有任何明显的错误
我们可以认为LLVM是一个完整的编译器架构,也可以认为它是一个用于开发编译器、解释器相关的库
Clang是一个C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器
Clang其实大致上可以对应到编译器的前端,主要处理一些和具体机器无关的针对语言的分析操作;编译器的优化器部分和后端部分是LLVM后端(狭义的LLVM);而整体的Compiler架构就是LLVM架构
- 总结:LLVM是Apple官方支持的编译器,而该编译器的前端是Clang,这两个工具都被集成到了Xcode里面
LLDB调试器已经取代了GDB,成为了Xcode工程中默认的调试器。它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。LLDB为Xcode提供了底层调试环境,其中包括内嵌在Xcode IDE中的位于调试区域的控制面板,在这里我们可以直接调用LLDB命令
1.Xcode控制台调试命令
想要看所有的调试命令,可以输入help,就会列出所有的调试命令
几个常用命令
p 命令是 print 命令的简写,使用p 命令可以查看基本数据类型的值,但是如果 使用 p 命令 查看的是对象,那么只会返回对象的指针地址。
p 命令后面除了可以接 变量、常量,还可以接 表达式。(但是不可以使用宏)
po 命令可以理解为打印对象。功能与 p 命令类似,所以也是可以打印 常量、变量,打印表达式返回的对象等(也不可以打印宏)
expr 是 expression 的简写, 使用expr 命令,能够在调试时,动态的执行赋值表达式,同时打印出结果
bt命令 可以打印出当前线程的堆栈信息
bt all 命令是打印所有线程的堆栈信息
2. log 打印
在debug模式下输出数据,并且输出在哪个类,第几行
#ifdef DEBUG
# define NSLog(fmt, ...) {NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__);}
#else
# define NSLog(...)
#endif
__PRETTY_FUNCTION__跟__FUNCTION__的功能相同.提供类名和函数名称的输出
__LINE__ 就是该行代码行号,用于日志中,方便查错
__VA_ARGS_ 宏定义中参数列表的最后一个参数为省略号(也就是三个点)。这样预定义宏_ _VA_ARGS_ _就可以被用在替换部分中,以表示省略号代表什么
3. 关于断点调试
Xcode 中的断点有普通断点、条件断点、符号断点、异常断点等很多种
-
普通断点
在对应行打断点是调试时最常用的调试方法
-
条件断点
设定条件可以控制代码在满足条件时断住,如在for 循环中。如果我们需要在i = 3 时添加断点,其他时候不加,那么就可以使用条件断点
-
符号断点
其实是针对某一个特定函数的断点
-
异常断点
打一个异常断点,这样崩溃时就会触发断点,很容易定位到问题所在,也能看到更多的崩溃相关信息,如Log,函数调用栈
Objective-C中处理异常是依赖于NSException实现的,它是异常处理的基类,它是一个实体类,而并非一个抽象类,所以你可以直接使用它或者继承它扩展使用,其实控制台输出的日志信息就是NSException产生的(如下)
当某段代码可能存在崩溃的危险,那么你就可以通过捕获异常来防止程序的崩溃
4.系统提供异常捕获方法
崩溃类型
应用层存在bug ,即OC程序崩溃
如 数组越界,selector方法没实现等抛出一系列NSException
可通过系统API 注册UncaughtNSException处理函数捕捉,定位比较容易违反系统规则而出错
如 watchdog超时,访问了不属于本进程的内存地址,用户强制退出,低内存终止等,系统抛出unix信号,但没有错误堆站信息
可通过注册信号处理函数捕捉,但只能补拙有限的几种类型,定位较困难
NSSetUncaughtExceptionHandler
OS SDK中提供了一个现成的函数 NSSetUncaughtExceptionHandler 用来做异常处理,但功能非常有限,而引起崩溃的大多数原因如:内存访问错误,重复释放等错误就无能为力了。 因为这种错误它抛出的是Signal,如果要处理它,我们还要利用unix标准的signal机制,注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数
什么是Signal
在计算机科学中, 信号 ( 英语: Signals)是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数
信号处理函数可以通过 signal() 系统调用来设置。如果没有为一个信号设置对应的处理函数,就会使用默认的处理函数,否则信号就被进程截获并调用相应的处理函数。在没有处理函数的情况下,程序可以指定两种行为:忽略这个信号 SIG_IGN 或者用默认的处理函数 SIG_DFL 。但是有两个信号是无法被截获并处理的: SIGKILL、SIGSTOP
( SignalHandler不要在debug环境下测试。因为系统的debug会优先去拦截。我们要运行一次后,关闭debug状态。应该直接在模拟器上点击我们build上去的app去运行。而UncaughtExceptionHandler可以在调试状态下捕捉
虽然Xcode屏蔽了signal的回调,我们只要在lldb中输入以下命令,signal的回调就可以进来了
pro hand -p true -s false SIGABRT )
Signal信号的类型
- SIGABRT--程序中止命令中止信号
- SIGALRM--程序超时信号
- SIGFPE--程序浮点异常信号
- SIGILL--程序非法指令信号
- SIGHUP--程序终端中止信号
- SIGINT--程序键盘中断信号
- SIGKILL--程序结束接收中止信号
- SIGTERM--程序kill中止信号
- SIGSTOP--程序键盘中止信号
- SIGSEGV--程序无效内存中止信号
- SIGBUS--程序内存字节未对齐中止信号
- SIGPIPE--程序Socket发送失败中止信号
5.关于符号表dSYM文件
1)符号表是什么
- 符号表就是指在Xcode项目编译后,在编译成的二进制文件.app同级目录下生成的同名的.dSYM文件
- .dSYM文件其实是一个目录,在子目录中包含了一个16进制的保存函数地址映射信息的中转文件,所有Debug的symbols都在这个文件中(包括文件名、函数名、行号等),所以也称之为调试符号信息文件
- 一般地,Xcode项目每次编译后,都会生成一个新的.dSYM文件。因此,App的每一个发布版本,都需要备份一个对应的.dSYM文件,以便后续调试定位问题
注意:
项目每一次编译后,.app和.dSYM成对出现,并且二者有相同的UUID值,以标识是同一次编译的产物。
UUID值可以使用dwarfdump —uuid来检查
- $ dwarfdump --uuid XX.app.dSYM
- $ dwarfdump --uuid XX.app/XX
- 符号表怎么生成
一般,Xcode项目默认的配置是在编译后生成.dSYM,在Build Setting配置如下:
- Generate Debug Symbols = Yes
Debug Information Format = DWARF with dSYM File
下面是几种常用的编译打包方式:
- 使用xcodebuild编译打包
在Xcode中编译项目后,会在工程目录下的build/ConfigurationName-iphoneos目录下生成.app和.app.dSYM文件。
如果使用xcodebuild命令进行编译打包,则可以指定编译结果的存储路径,同样会有.app和.app.dSYM生成。
一般地,我们推荐打包发布时,使用xcodebuild编译打包,方便.app和.app.dSYM的匹配存储,避免.app.dSYM文件丢失的情况。
- 使用Xcode的Archive导出
如果开发者使用Xcode的Archive导出功能打包,可以切换到Organizer的Projects视图,查看对应项目的Derived Data路径,在其中可以找到当前导出过程产生的.app和.app.dSYM文件
- 使用make编译打包
如果开发团队不使用Xcode编译打包,而是使用make编译生成.o文件,然后打包发布。此时,编译过程不会有.dSYM文件生成。开发者可以使用dsymutil工具从.o文件中提取符号信息
- dSYM 文件有什么作用
在前面的内容可以知道,符号表的作用是把崩溃中的函数地址解析为函数名等信息。 如果开发者能够获取到崩溃的函数地址信息,就可以利用符号表分析出具体的出错位置
例如,崩溃问题的函数地址堆栈如下:
错误地址堆栈
- 3 CoreFoundation 0x254b5949 0x253aa000 + 1096008
- 4 CoreFoundation 0x253e6b68 _CF_forwarding_prep_0 + 24
- 5 SuperSDKTest 0x0010143b 0x000ef000 + 74808
符号化堆栈
- 3 CoreFoundation 0x254b5949 <redacted> + 712
- 4 CoreFoundation 0x253e6b68 _CF_forwarding_prep_0 + 24
- 5 SuperSDKTest 0x0010143b -[ViewController didTriggerClick:] + 58
说明:
大部分情况下,开发者能获取到的都是错误地址堆栈,需要利用符号表进一步符号化才能分析定位问题。
部分情况下,开发者也可以利用backtrace看到符号化堆栈,可以大概定位出错的函数、但却不知道具体的位置。
通过利用符号表信息,也是可以进一步得到具体的出错位置的。
目前,许多崩溃监控服务都显示backtrace符号化堆栈,增加了可读性,但分析定位问题时,仍然要进一步符号化处理。
- 符号化
将这些十六进制地址转化成方法名称和行数的过程称之为符号化
(1)使用symbolicatecrash,Xcode自带的崩溃分析工具
symbolicatecrash是一个将堆栈地址符号化的脚本,输入参数是苹果官方格式的崩溃日志及本地的.dSYM文件,使用symbolicatecrash工具的限制就在于只能分析官方格式的崩溃日志,需要从具体的设备中导出,获取和操作都不是很方便,而且,符号化的结果也是没有具体的行号信息的,也经常会出现符号化失败的情况。
实际上,使用Xcode查看崩溃日志是已经符号化的,Xcode能够自动根据本地存储的.dSYM文件进行了符号化的操作,是因为内置了symbolicatecrash
(2)dSYM 工具
6.崩溃日志分析
crash日志,几个重要的关键词
关键词解释:
6.1、 进程信息
第一部分是闪退进程的相关信息:
Incident Identifier : 是崩溃报告的唯一标识符。
CrashReporter Key: 是与设备标识相对应的唯一键值。同一个设备上同一版本的App发生Crash时,该值都是一样的。虽然它不是真正的设备标识符,但也是一个非常有用的情报:如果你看到100个崩溃日志的CrashReporter Key值都是相同的,或者只有少数几个不同的CrashReport值,说明这不是一个普遍的问题,只发生在一个或少数几个设备上。
Hardware Model :标识设备类型。 如果很多崩溃日志都是来自相同的设备类型,说明应用只在某特定类型的设备上有问题。上面的日志里,崩溃日志产生的设备是iPhone 4s。
Process:代表Crash的进程名称,通常都是我们的App的名字, []里面是当时进程的ID
Path:崩溃文件的路径
Identifier:项目标识符,就是Bundle Id
Version:版本号
Code Type:当前App的CPU架构
Parent Process:当前进程的父进程,由于iOS中App通常都是单进程的,一般父进程都是launchd
.....等等.......
6.2、基本信息
这部分给出了一些基本信息,包括闪退发生的日期Date/Time和时间Launch Time,设备的iOS版本OS Version等。
Report Version-Crash日志的格式,目前基本上都是104,不同的version里面包含的字段可能有不同
6.3、异常信息
Exception Type:异常的类型。
Exception Codes :异常错误码
Termination Reason:闪退的原因,比如常见的数组越界啊,什么的。
Triggered by Thread:出现问题在哪个线程,这个比较重要,首先确定在哪个线程中出了问题,然后再去定位。
6.4、线程回溯
这部分提供应用中所有线程的回溯日志。回溯是闪退发生时所有活动帧清单。它包含闪退发生时调用函数的清单. 线程调用的一些堆栈信息,压根看不懂,所有需要进行符号化处理。看下面这行日志:
12 UIKit 0x00000001943bd7fc 0x194343000 + 501756
它包括四列:
帧编号—— 此处是12。
二进制库的名称 ——此处是 UIKit
调用方法的地址 ——此处是 0x00000001943bd7fc
第四列分为两个子列,一个基本地址和一个偏移量。此处是0x194343000 + 501756, 第一个数字指向文件,第二个数字指向文件中的代码行。
6.5 二进制映像(Binary Images)
这部分列出了当Crash发生时被装载进进程内存空间的依赖库或者模块
7.异常类型信息
Exception Type
1)EXC_BAD_ACCESS
此类型的Excpetion是我们最长碰到的Crash,通常用于访问了不改访问的内存导致。一般EXC_BAD_ACCESS后面的"()"还会带有补充信息。
SIGSEGV: 通常由于重复释放对象导致,这种类型在切换了ARC以后应该已经很少见到了。
SIGABRT: 收到Abort信号退出,SIGABRT 异常是由于某个对象接收到未实现的消息引起的。
SEGV:(Segmentation Violation),代表无效内存地址,比如空指针,未初始化指针,栈溢出等;
SIGBUS:总线错误,与 SIGSEGV 不同的是,SIGSEGV 访问的是无效地址,而 SIGBUS 访问的是有效地址,但总线访问异常(如地址对齐问题)
SIGILL:尝试执行非法的指令,可能不被识别或者没有权限
2)EXC_BAD_INSTRUCTION
此类异常通常由于线程执行非法指令导致
3)EXC_ARITHMETIC
除零错误会抛出此类异常
Exception Code 异常编码
前面日至的第3部分找到异常编码。有些编码比较常见。通常,异常编码以一些文字开头,紧接着是一个或多个十六进制值,此数值正是说明闪退根本性质的所在。 从这些编码中,可以区分出闪退是因为程序错误、非法内存访问或者是其他原因
0xbaaaaaad 此种类型的log意味着该Crash log并非一个真正的Crash,它仅仅只是包含了整个系统某一时刻的运行状态。通常可以通过同时按Home键和音量键,可能由于用户不小心触发
常见的异常编码:
0xbad22222 当VOIP(指在 IP 网络上使用 IP 协议以数据包的方式传输语音)程序在后台太过频繁的激活时,系统可能会终止此类程序
0x8badf00d 程序启动或者恢复时间过长被watch dog终止,通常是应用花费太多时间而无法启动、终止或响应用系统事件
0xc00010ff 程序执行大量耗费CPU和GPU的运算,导致设备过热,触发系统过热保护被系统终止
0xdead10cc 程序退到后台时还占用系统资源,如通讯录被系统终止
0xdeadfa11 程序无响应,用户强制关闭
(注意: 在后台任务列表中关闭已挂起的应用不会产生崩溃日志。 一旦应用被挂起,它何时被终止都是合理的。所以不会产生崩溃日志。)
低内存崩溃
低内存崩溃日志与其他类型的崩溃日志很不一样,它们不指向特定的文件和代码行。相反,它们画出了闪退时设备上的内存使用情况的图表
头部还是跟其他崩溃日志很像的: 提供了 Incident Identifier, CrashReporter Key, Hardware Model, OS Version等信息。
接下来部分是低内存崩溃日志特有的:
Free pages 指可用内存页数。每页大小约是4KB, 上面的日志中,可用内存约为3,872 KB (或者说 3.9 MB)。
Purgeable pages 是那部分可被清除或重用的内存。在上面的日志中,是0KB。
Largest process是闪退时使用大部分内存的应用名称,在上面的日志中,正是你的应用!
Processes显示了闪退时各进程列表,还包含内存使用量。包含进程名 (第一列), 进程唯一标识符(第二名), 进程使用的内存页数(第三列)。最后一列是每个应用的状态。通常,发生闪退的应用的状态是 frontmost。 这里是 Rage Masters, 使用28591 页 (or 114.364 MB) 内存——这内存太多了!