iOS-APP崩溃分析

※ 背景

  • 崩溃是指操作系统向正在异常运行的app发送的信号,程序崩溃是宣判app死亡的直接因素,当我们在做iOS开发积攒到一定经验后,我们需要考虑的是面对我们工程中各种各样引起app崩溃的问题,仔细看工程崩溃日志,会有一些相应的信号输出给我们
  • Xcode崩溃日志首先给我们提供的就是崩溃类型,通过一定开发经验的积攒,根据崩溃类型我们可以快速的思考到与其类型相对应的崩溃场景,那么都有哪些崩溃类型呢?
  • 在应用层面及实际开发场景中,哪些操作不当会引起app崩溃呢?

一、崩溃种类场景

APP的崩溃可以分为两类:信号可捕捉崩溃 和 信号不可捕捉崩溃。

信号可捕捉的崩溃

  • 数组越界:取数据时候索引越界,APP发生崩溃。给数组添加nil会崩溃。
  • 多线程问题:多个线程进行数据的存取,可能会崩溃。例如有一个线程在置空数据的同时另一个线程在读取数据。
  • 野指针问题:指针指向一个已删除的对象访问内存区域时,会出现野指针崩溃。野指针问题是导致 App 崩溃的最常见,也是最难定位的一种情况。
  • NSNotification线程问题:NSNotification 有很多种线程实现方式,同步、异步、聚合,所以不恰当的线程发送和接收会出现崩溃问题。
  • KVO问题:‘If your app targets iOS 9.0 and later or OS X v10.11 and later, you don’t need to unregister an observer in its deallocation method。’ 在9.0之前需要手动remove 观察者,如果没有移除会出现观察者崩溃情况。
  • 内存管理不当问题:

信号不可捕捉的崩溃

  • 后台任务超时
  • App超过系统限制的内存大小被杀死
  • 主线程卡顿被杀死

二、崩溃日志

1、什么情况下会产生崩溃日志?

  • 两种主要情况会产生崩溃日志:
    • 1.应用违反操作系统规则。
    • 2.应用中有Bug。

违反操作系统规则

  • 包括在启动、恢复、挂起、退出时watchdog超时、用户强制退出和低内存终止。
  • Watchdog 超时机制
    从iOS 4.x开始,退出应用时,应用不会立即终止,而是退到后台。但是,如果你的应用响应不够快,操作系统有可能会终止你的应用,并产生一个崩溃日志。这些事件与下列UIApplicationDelegate方法相对应:
    application:didFinishLaunchingWithOptions:
    applicationWillResignActive:
    applicationDidEnterBackground:
    applicationWillEnterForeground:
    applicationDidBecomeActive:
    applicationWillTerminate:
    上面所有这些方法,应用只有有限的时间去完成处理。如果花费时间太长,操作系统将终止应用。
    注意: 如果你没有把需要花费时间比较长的操作(如网络访问)放在后台线程上就很容易发生这种情况。

应用中有bug

三、解析符号化后崩溃报告

  • 通过 symbolicatecrash 工具来解析崩溃日志。
    你可以通过以下命令找到本机的 symbolicatecrash 工具位置
    find /Applications/Xcode.app -name symbolicatecrash -type f
    可以把它拷贝出来, 然后通过以下命令来解析崩溃日志。
./symbolicatecrash ./xxx.crash ./xxx.app.dSYM > symbol.crash

1、头部关键信息

  • Incident Identifier: 一个唯一的标识. 不会存在共用一个标识的崩溃报告.
  • CrashReporter Key:是与设备标识相对应的唯一键值。虽然它不是真正的设备标识符,但也是一个非常有用的情报:如果你看到100个崩溃日志的CrashReporter Key值都是相同的,或者只有少数几个不同的CrashReport值,说明这不是一个普遍的问题,只发生在一个或少数几个设备上。
  • Process: 是应用名称。中括号里面的数字是闪退时应用的进程ID。
  • Version: 崩溃进程的版本号. 这个值包含在 CFBundleVersion and CFBundleVersionString中.
  • Code Type: 崩溃日志所在设备的架构. 会是ARM-64, ARM, x86-64, or x86中的一个.
  • OS Version: 崩溃发生时的系统版本

2、异常信息中的关键字段

  • Exception Codes: 处理器的具体信息有关的异常编码成一个或多个64位进制数。通常情况下,这个区域不会被呈现,因为将异常代码解析成人们可以看懂的描述是在其它区域进行的。
  • Exception Subtype: 供人们可读的异常代码的名字
  • Exception Message: 从异常代码中提取的额外的可供人们阅读的信息.
  • Exception Note: 不是特定于一个异常类型的额外信息.如果这个区域包含SIMULATED (这不是一个崩溃)然后进程没有崩溃,但是被watchdog杀掉了
  • Termination Reason: 当一个进程被终止的时的原因。
  • Triggered by Thread: 异常所在的线程.

3、其他常见的异常

  • Bad Memory Access [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]
    进程试图访问无效的内存,或试图以内存的保护级别所不允许的方式去访问内存(例如,写入到只读存储器)。异常类型字段(Exception Subtype)包含一个kern_return_t描述错误,和错误的访问的内存地址。这里是调试一个Bad Memory Access的一些小技巧:
    • (1).如果objc_msgSend或者objc_release在回溯(Backtraces)的顶部附近,这个进程可能是尝试给一个释放的对象发送消息。你应该用Zombies instrument(调试僵尸对象的工具)来更好的理解这个崩溃。
    • (2).如果gpus_ReturnNotPermittedKillClient在回溯的顶部附近,这个进程是由于在后台尝试用OpenGL ES 或者 Metal来渲染,而被杀掉的。See QA1766: How to fix OpenGL ES application crashes when moving to the background.
    • (3).用 Address Sanitizer (xcode7引入的新特性)来跑程序。
      The address sanitizer adds additional instrumentation around memory access in your compiled code. As your application runs, Xcode will alert you if memory is accessed in a way that could lead to a crash.
  • Abnormal Exit (异常退出)[EXC_CRASH // SIGABRT]
    进程异常退出。该异常类型崩溃最常见的原因是未捕获的Objective-C和C++异常和调用abort()。
    如果他们需要太多的时间来初始化,程序将被终止,因为触发watchdog。如果是因为启动的时候被挂起,所产生的崩溃报告异常类型(Exception Subtype)将是launch_hang。
  • Trace Trap (追踪捕获)[EXC_BREAKPOINT // SIGTRAP]
    类似于异常退出,这个异常是为了给附加的调试器中断的过程的机会在其执行一个特定的点。您可以通过主动调用__builtin_trap()函数引发此异常使用,如果没有调试器连接,进程将被终止并生成崩溃报告。
  • Illegal Instruction(非法指令) [EXC_BAD_INSTRUCTION // SIGILL]
    进程试图执行非法或未定义指令。这个进程可能试图通过一个配置错误的函数指针,跳到一个无效的地址。
  • Resource Limit [EXC_RESOURCE]
    这个进程超出了资源消耗的限制。这是一个从操作系统通知,进程是使用太多的资源。这虽然不是崩溃但也会生成崩溃日志。
  • 其它的异常信息
    • 0x8badf00d: 读做 “ate bad food”! (把数字换成字母,是不是很像 :p)该编码表示应用是因为发生watchdog超时而被iOS终止的。通常是应用花费太多时间而无法启动、终止或响应用系统事件。
    • 0xbad22222: 该编码表示 VoIP 应用因为过于频繁重启而被终止。
    • 0xdead10cc: 读做 “dead lock”!该代码表明应用因为在后台运行时占用系统资源,如通讯录数据库不释放而被终止 。
    • 0xdeadfa11: 读做 “dead fall”! 该代码表示应用是被用户强制退出的。根据苹果文档, 强制退出发生在用户长按开关按钮直到出现 “滑动来关机”, 然后长按 Home按钮。强制退出将产生 包含0xdeadfa11 异常编码的崩溃日志, 因为大多数是强制退出是因为应用阻塞了界面。

4、线程回溯

如以下 线程帧 记录了 这些信息

2 
YDTCMoments 
0x34648e88 
0x83000 + 8740
  • 帧编号—— 此处是2。(数子从大到小为发生的顺序)
  • 二进制库的名称 ——此处是 YDTCMoments.
  • 调用方法的地址 ——此处是 0x34648e88.
  • 第四列分为两个子列,一个基本地址和一个偏移量。此处是0×83000 + 8740, 第一个数字指向文件,第二个数字指向文件中的代码行。

四、崩溃信号

Defined in header <signal.h>

#define SIGTERM /*implementation defined*/
#define SIGSEGV /*implementation defined*/
#define SIGINT /*implementation defined*/
#define SIGILL /*implementation defined*/
#define SIGABRT /*implementation defined*/
#define SIGFPE /*implementation defined*/
... ...
  • 常看到的崩溃信号有:SIGTERM、SIGSEGV、SIGINT、SIGILL、SIGABRT、SIGFPE、SIGBUS、SIGTRAP、EXC_BAD_ACCESS、EXC_ARITHETIC、(watchdog(看门狗)、OOM)等等

SIGTERM

  • 程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。
  • iOS中一般不会处理到这个信号

SIGSEGV

  • 段错误信息:invalid memory access (segmentation fault),无效的内存地址引用信号,是操作系统产生的一个较为严重的问题。硬件出错(一般不常见)、访问不可读的内存地址、或者向受保护的内存地址写入数据时会发生此错误。
  • 默认情况下,代码页不允许进行写操作,当app中某个指针指向了代码页并试图修改对应位置的值时,会收到此崩溃类型。当要读取一个指针的值时,而它被初始化成指向无效内存地址的垃圾值时也会收到此崩溃类型。
  • 此类型的崩溃调试起来略困难,导致此崩溃类型最常见的原因是:不正确的类型转换。要避免过度使用指针或尝试手动修改指针来读取私有数据结构,否则在修改指针时需要注意内存对齐和填充问题,处理不当就会收到此崩溃类型
  • iOS中的场景:常见的野指针访问:
    非ARC模式下,iOS中经常会出现在 Delegate对象野指针访问
    ARC模式下,iOS经常会出现在Block代码块内 强持有可能释放的对象

SIGINT

  • external interrupt, usually initiated by the user
    通常由用户输入的整型中断信号
  • 在iOS中一般不会处理到该信号

SIGILL

  • 非法指令信号(signal illegal instruction):invalid program image, such as invalid instruction,不管在任何情况下得杀死进程的信号,当在处理器上执行非法指令时会发生。
  • 执行非法指令是指 将函数指针指向另外一个函数时,但该函数指针出了问题,指向了一段已经释放的内存或一个数据段。有时候收到的信号SIGILL或者是EXC_BAD_INSTRUCTION是一回事,不过EXC_*此类信号是不依赖于体系结构的通用领域信号。
  • iOS中的场景:由于iOS应用程序平台的限制,在iOS APP内禁止kill掉进程,所以一般不会处理

SIGABRT

  • 中断信号(SIGNAL ABORT):abnormal termination condition, as is e.g. initiated by abort(),通常由于异常引起的中断信号,异常发生时系统会调用abort()函数发出该信号。当操作系统发现不安全的情况时,此类型号会进行更多的控制,必要的话他会要求进程进行清理工作。当它出现时控制台通常会输出大量的信息,来说明具体哪里出了问题。由于它是可控制的崩溃,所以在LLDB时输入bt命令可以打印回溯信息。
  • iOS中场景:一种是由于方法调用错误(调用了不能调用的方法),一种是由于数组访问越界的问题

SIGFPE

  • erroneous arithmetic operation such as divide by zero
    浮点数异常的信号通知
  • iOS中的场景:一般是由于 除数为0引起的

SIGBUS

  • 总线错误信号:代表无效内存访问,即访问的内存是个无效的内存地址(地址指向的位置不是物理内存地址,可能是某硬件芯片的地址)

SIGTRAP

  • 陷阱信号:并不是一个真正的崩溃信号。会在CPU执行trap指令发送。LLDB调试器通常会处理此信号,并在指定的断点处停止运行。(收到不明原因的此信号,一般先清除上次的输出,然后重新进行构建即可解决)

EXC_BAD_ACCESS

  • 野指针引起的崩溃:访问了一个已经释放的内存而导致,向已释放的对象发送消息时,就会出现。造成此崩溃最常见的原因是,在初始化方法中初始化变量时用错了所有权修饰符,导致对象过早被释放。
    • eg:在viewDidLoad方法中为UIViewController创建了一个包含元素的NSDictionary,将此字典所有权修饰符设置成assign而不是strong。这时如果在viewWillAppear中访问就会得到此类型的崩溃,原因访问的对象是已经释放掉的对象。
  • 这个崩溃发生时,查看崩溃日志往往得不到有用的栈信息,可以考虑使用NSZombieEnabled环境变量来解决此问题,它可以跟踪对象的释放过程,用于调试与内存相关的问题
    • NSZombieEnabled工作原理:APP启动NSZombie而不是让APP直接crash,一个错误的内存访问就会变成一条五分失败的消息发送给僵尸对象。僵尸对象会显示接收到的消息,然后跳入调试器,这样就可看到具体哪里出了问题。
    • NSZombieEabled设置:在Xcode->Product-Edit Scheme->打开scheme页面->Run选项的Diagnostics里面->设置NSZombieEnabled环境变量(勾选Zombie Objects)
    • 僵尸在ARC出现之前作用很大。自从有了ARC,如果开发过程中注意一下使用对象的所有权方面,通常不会碰到此内存相关的崩溃
  • iOS中的场景:野指针导致崩溃:
    • 1、对象释放后内存没被改动过,原来的内存保存完好,可能不Crash或者出现逻辑错误(随机Crash)。
    • 2、对象释放后内存没被改动过,但是它自己析构的时候已经删掉某些必要的东西,可能不Crash、Crash在访问依赖的对象比如类成员上、出现逻辑错误(随机Crash)。
    • 3、对象释放后内存被改动过,写上了不可访问的数据,直接就出错了很可能Crash在objc_msgSend上面(必现Crash,常见)。
    • 4、对象释放后内存被改动过,写上了可以访问的数据,可能不Crash、出现逻辑错误、间接访问到不可访问的数据(随机Crash)。
    • 5、对象释放后内存被改动过,写上了可以访问的数据,但是再次访问的时候执行的代码把别的数据写坏了,遇到这种Crash只能哭了(随机Crash,难度大,概率低)!!
    • 6、对象释放后再次release(几乎是必现Crash,但也有例外,很常见)。

EXC_ARITHETIC

  • iOS中 一般在除零时app会收到此通用领域的信号。同:SIGFPE

watchdog

  • 看门狗超时:此类崩溃比较容易分辨,因为错误码是固定的0x8badf00d。它经常出现在执行同步网络调用而阻塞主线程的情况。

OOM

  • 低内存崩溃(out-of-memory):字面意思就是内存超过了限制。它是由于 iOS 的 Jetsam机制造成的一种“另类” Crash,它不同于常规的Crash,通过Signal捕获等Crash监控方案无法捕获到OOM事件。
    当然还会有FOOM/BOOM,其中FOOM代表的是Foreground-out-of-memory,是指App在前台因消耗内存过多引起系统强杀。这也就是本文要讨论的。后台出现OOM不一定都是app本身造成的,大多数是因为当前在前台的App占用内存过大,系统为了保证前台应用正常运行,把后台应用清理掉了

五、自定义代码来获取崩溃日志信息

swift:

NSSetUncaughtExceptionHandler {
    
     exception in
	print("exception: \(exception.callStackSymbols)")
}

OC:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    
    
    NSSetUncaughtExceptionHandler(&caughtExceptionHandler);
    /*
     Changes the top-level error handler.
      Sets the top-level error-handling function where you can perform last-minute logging before the program terminates
    */
    return YES;
}

void caughtExceptionHandler(NSException *exception){
    
    
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@",name,reason,[callStack componentsJoinedByString:@"\n"]];
    
    //把异常崩溃信息发送至开发者邮件
    NSMutableString *mailUrl = [NSMutableString string];
    [mailUrl appendString:@"mailto:[email protected]"];
    [mailUrl appendString:@"?subject=程序异常崩溃信息,请配合发送异常报告,谢谢合作!"];
    [mailUrl appendFormat:@"&body=%@", content];
    // 打开地址
    NSString *mailPath = [mailUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:mailPath]];
}

※总结

  • 1,其实对于iOS中app端崩溃了解这些崩溃及底层逻辑可更快定位问题
  • 2,对应用层面更多的表现为:越界,early release,double free,内存管理不当等

iOS开发这么些年一直没去整理过这些东西,现在静下心来开始去整理,后续会持续更新,如果内容有用感谢点赞,如有问题欢迎评论留言

猜你喜欢

转载自blog.csdn.net/MOON_YZM/article/details/127553830