Jerry's Blog

Less is more, Live and learning

Hello,I'm an iOS developer in China.


App常见崩溃分析

前言

在iOS开发调试过程中,我们会遇到很多崩溃问题,比如数组越界、容器中插入nil、或调用不存在的方法时都会出现崩溃现象。那么,为了能更好的应对并避免这些常见崩溃问题的发生,就是我们亟待需要解决的问题。下面我们将分析iOS开发中常见的几个崩溃,并结合示例给出这类问题的解决方案。

NSInvalidArgumentException

从字面上来看,是无效的参数异常,但是触发这个异常的场景还有很多,最常见的就是在NSArray,NSDictionary容器中插入nil时发生,例如下面代码所示:

// 示例代码
NSString *password = nil;
NSDictionary *dict = @{
                       @"userName": @"bruce",
                       @"password": password
                       };
NSLog(@"dict is : %@", dict);

// 崩溃日志
***Terminating app due to uncaught exception 'NSInvalidArgumentException',reason:'*** -[NSPlaceholderDictionary initWithObjects:forKeys:count]: attemp to insert nil object from objects[1]'

这种崩溃原因,从控制台输出的异常信息就可以直观的看出来,是给NSDictionary 插入了nil对象造成的崩溃。在日常开发中,如果对方法入参没有做判空处理,或是后台返回的json数据中包含nil时就会造成此异常。那么如何防止这种崩溃的发生呢?

解决方案一

对指定的方法入参进行判空处理,对后台返回的数据转model的时候,增加null空值判断,或是封装为NSNull对象进行存储。

解决方案二

代码改动量少,无侵入性的方案,就是利用Runtime 动态特性,swizzle Method 方式hook住NSArray、NSDictionary 等方法的插入对象时的系统原生方法,然后注入对空对象nil 的检查并封装为NSNull 的ObjC对象,这样就可以避免程序的直接崩溃。实例代码如下:

// 下列方法为Method swizzling 交换后的方法实现
+ (instancetype)gl_dictionaryWithObjects:(const id [])objects forKeys:(const id<NSCopying> [])keys count:(NSUInteger)cnt {
    id safeObjects[cnt];
    id safeKeys[cnt];
    NSUInteger j = 0;
    for (NSUInteger i = 0; i < cnt; i++) {
        id key = keys[i];
        id obj = objects[i];
        if (!key) {
            continue;
        }
        if (!obj) {
            obj = [NSNull null];
        }
        safeKeys[j] = key;
        safeObjects[j] = obj;
        j++;
    }
    return [self gl_dictionaryWithObjects:safeObjects forKeys:safeKeys count:j];
}

unrecognized selector sent to instance xxx 异常

这种异常很常见,意思是给一个对象发送了一个自己没有的消息,也就是说调用了一个不属于自己的方法。由于Objc有一个消息转发机制,当在该对象的类方法列表中找不到,所调用的方法时,_objc_msgForward就会触发消息转发,如果在转发过程中任然没有得到方法的实现,那么就会抛出unrecognized selector 异常,可参考这篇文章了解消息转发

NSRangeException

当代码中试图去访问对象的范围内不存在的索引的时候,根据堆栈信息可以追溯到这个异常,比如:

[__NSArrayM objectAtIndex:]: index 11 beyond bounds [0 .. 10]

大多数情况下引起这个问题的原因是数组和字符串,比如:

NSArray *arr = @[@1, @2, @3, @4];
NSNumber *num = [arr objectAtIndex:9]; // 抛出异常

NSString *mainString = @”myString;
NSString *subString = [mainString substringToIndex:24]; // 抛出异常

避免这种异常很简单,及时确保索引在对象的范围内,比如:

NSNumber *obj = nil; 
NSArray *arr = @[@1, @2, @3, @4];

if([arr count] > 9) {
    obj = [arr objectAtIndex:9];
}

NSString *subString = nil; 
NSString *mainString = @”myString;

if([mainString length] > 24) {
    subString = [mainString substringToIndex:24];
}

EXC_BAD_ACCESS

在访问一个已经释放的对象或向它发送消息时,EXC_BAD_ACCESS就会出现。造成EXC_BAD_ACCESS最常见的原因是,在初始化方法中初始化变量时用错了所有权修饰符,这会导致对象过早地被释放。举个例子,在viewDidLoad方法中为UIViewController创建了一个包含元素的NSArray,却将该数组的所有权修饰符设成了assign而不是strong。现在在viewWillAppear中,若要访问已经释放掉的对象时,就会得到名为EXC_BAD_ACCESS的崩溃。

这个崩溃发生时,查看崩溃日志,却往往得不到有用的栈信息。还好,有一个方法用来解决这个问题:NSZombieEnabled。

这是一个环境变量,用来调试与内存相关的问题,跟踪对象的释放过程。启用了NSZombieEnabled的话,它会用一个僵尸实现来去你的默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现会将该对象转换成僵尸对象。僵尸对象的作用是在你向它发送消息时,它会显示一段日志并自动跳入调试器。

所以,当在应用中启用NSZombie而不是让应用直接崩溃时,一个错误的内存访问就会变成一条无法识别的消息发送给僵尸对象。僵尸对象会显示接收到的消息,然后跳入调试器,这样你就可以查看到底哪时出了问题。 可以在Xcode的scheme页面中设置NSZombieEnabled环境变量。点击Product—>Edit Scheme打开该页面,然后勾选Enable Zombie Objects复选框,如图所示:

僵尸在ARC出现以前作用很大。但自从有了ARC,如果你在对象的所有权方面比较注意,那么通常不会碰到内存相关的崩溃。

SIGSEGV 异常

段错误信息(SIGSEGV)是操作系统产生的一个更严重的问题。当硬件出现错误、访问不可读的内存地址或向受保护的内存地址写入数据时,就会发生这个错误。

硬件错误这一情况并不常见。当要读取保存在RAM中的数据,而该位置的RAM硬件有问题时,你会收到SIGSEGV。SIGSEGV更多是出现在后两种情况。默认情况下,代码页不允许进行写操作,而数据而不允许进行执行操作。当应用中的某个指针指向代码页并试图修改指向位置的值时,你会收到SIGSEGV。当要读取一个指针的值,而它被初始化成指向无效内存地址的垃圾值时,你也会收到SIGSEGV。

SIGSEGV错误调试起来更困难,而导致SIGSEGV的最常见原因是不正确的类型转换。要避免过度使用指针或尝试手动修改指针来读取私有数据结构。如果你那样做了,而在修改指针时没有注意内存对齐和填充问题,就会收到SIGSEGV。

举个常见的例子,在使用代理的时候:

// 代理应该是用weak修饰的
self.delegate = myView;

// myView从UINavigationController中Pop之后就会被销毁,而self.delegate仍然起作用,成了野指针

// 将会抛出异常
[self.delegate doSomething];

避免这种异常可以在调用之前检查一下代理是否为空,是否能够响应所给的Selector

if(self.delegate != nil) {
    if([self.delegate respondsToSelector:@selector(doSomething)]) {
        [self.delegate doSomething];
    }
}

SIGABRT

SIGABRT代表SIGNAL ABORT(中止信号)。当操作系统发现不安全的情况时,它能够对这种情况进行更多的控制;必要的话,它能要求进程进行清理工作。在调试造成此信号的底层错误时,并没有什么妙招。Cocos2d或UIKit等框架通常会在特定的前提条件没有满足或一些糟糕的情况出现时调用C函数abort(由它来发送此信号)。当SIGABRT出现时,控制台通常会输出大量的信息,说明具体哪里出错了。由于它是可控制的崩溃,所以可以在LLDB控制台上键入bt命令打印出回溯信息。

SIGBUS

总线错误信号(SIGBUG)代表无效内存访问,即访问的内存是一个无效的内存地址。也就是说,那个地址指向的位置根本不是物理内存地址(它可能是某个硬件芯片的地址)。SIGSEGV和SIGBUS都羽毛球EXC_BAD_ACCESS的子类型。

SIGTRAP

SIGTRAP代表陷阱信号。它并不是一个真正的崩溃信号。它会在处理器执行trap指令发送。LLDB调试器通常会处理此信号,并在指定的断点处停止运行。如果你收到了原因不明的SIGTRAP,先清除上次的输出,然后重新进行构建通常能解决这个问题。

SIGILL

SIGILL代表signal illegal instruction(非法指令信号)。当在处理器上执行非法指令时,它就会发生。执行非法指令是指,将函数指针会给另外一个函数时,该函数指针由于某种原因是坏的,指向了一段已经释放的内存或是一个数据段。有时你收到的是EXC_BAD_INSTRUCTION而不是SIGILL,虽然它们是一回事,不过EXC_*等同于此信号不依赖体系结构。

参考资料

iOS中的崩溃类型

iOS 崩溃Crash解析

最近的文章

iOS数据埋点方案

代码埋点代码埋点是一种常规且直观的方案,需要开发人员在需要埋点的页面或点击事件的响应方法中注入埋点统计相关方法进行数据统计上报。也可以接入三方统计分析SDK来实现,比如友盟UMCAnalytics统计分析库。实现方式如下代码所示:// 通过在HomeViewController 中的“页面已显示”和“页面已消失”两个回调中注入统计代码实现埋点@implementation HomeViewController//...other methods- (void)viewDidAppear:(...…

iOS继续阅读
更早的文章

App启动分析与优化策略

启动过程分析 解析Info.plist 加载相关信息,例如闪屏 沙箱建立、权限检查 Mach-O加载 Mach-O 文件:我们写的程序想要跑起来,肯定它的可执行文件格式要被操作系统所理解。比如ELF是Linux下的可执行文件格式,那么对于OS X / iOS来说,Mach-O 是其可执行文件格式。 Mach-O格式主要包括以下几种文件类型: Executable:应用的主要二进...…

iOS继续阅读