iOS热门崩溃分析

前言

随着移动互联网的发展,各种App应用已经融入到我们的日常生活当中,应用的稳定性的要求也越来越高,首当其冲的就是应用的crash问题,轻则影响用户的良好体验,重则导致用户大量流失造成巨大的影响。所以解决crash是重要而紧急的事情。

2021年友盟+发布了移动应用性能体验报告,报告中指出App的整理崩溃率为0.29%,其中iOS端崩溃率为0.10%。以及热门崩溃排行榜:


崩溃排行榜

本文将通过热门崩溃产生的原因以及崩溃demo(写bug)来了解这些崩溃是如何产生的。热门崩溃占比是很高的,如果能解决这些热门崩溃,移动应用的质量会有很大的提升。

NSException

首先我们先来介绍一下NSException:


NSException

相信大家对这个页面不会陌生吧,这个日志就是NSException产生的,一旦程序抛出异常,程序就会崩溃,控制台就会输出这些崩溃日志。

NSException对象继承自NSObject,是专门用来抛出Objective-C异常的,有四个属性:

  • name:异常名称
  • reason:异常原因
  • userInfo:异常信息,字典形式
  • reserved:堆栈信息
@interface NSException : NSObject  {
    @private
    NSString        *name;
    NSString        *reason;
    NSDictionary    *userInfo;
    id          reserved;
}

当出现异常时,会抛出一个NSException对象,内容如上图所示。


异常

除了上面的属性外,NSException还预定义了一些通用的异常名称:

/***************    Generic Exception names     ***************/
/*
You should typically use a more specific exception name.
*/
FOUNDATION_EXPORT NSExceptionName const NSGenericException; 
/*
Name of an exception that occurs when attempting to access outside the bounds of some data, such as beyond the end of a string.
*/
FOUNDATION_EXPORT NSExceptionName const NSRangeException; 
/*
Name of an exception that occurs when you pass an invalid argument to a method, such as a nil pointer where a non-nil object is required.
*/
FOUNDATION_EXPORT NSExceptionName const NSInvalidArgumentException;
/*
Name of an exception that occurs when an internal assertion fails and implies an unexpected condition within the called code.
*/ 
FOUNDATION_EXPORT NSExceptionName const NSInternalInconsistencyException; 

/*
Obsolete; not currently used.
*/
FOUNDATION_EXPORT NSExceptionName const NSMallocException; 

/*
Name of an exception that occurs when a remote object is accessed from a thread that should not access it.
*/
FOUNDATION_EXPORT NSExceptionName const NSObjectInaccessibleException; 
/*
Name of an exception that occurs when the remote side of the NSConnection refused to send the message to the object because the object has never been vended.
*/
FOUNDATION_EXPORT NSExceptionName const NSObjectNotAvailableException; 
/*
Name of an exception that occurs when an internal assertion fails and implies an unexpected condition within the distributed objects.
*/
FOUNDATION_EXPORT NSExceptionName const NSDestinationInvalidException;
    
    /*
Name of an exception that occurs when a timeout set on a port expires during a send or receive operation.
*/
FOUNDATION_EXPORT NSExceptionName const NSPortTimeoutException;
/*
Name of an exception that occurs when the send port of an NSConnection has become invalid.
*/
FOUNDATION_EXPORT NSExceptionName const NSInvalidSendPortException;
/*
Name of an exception that occurs when the receive port of an NSConnection has become invalid.
*/
FOUNDATION_EXPORT NSExceptionName const NSInvalidReceivePortException;
/*
Generic error occurred on send.
*/
FOUNDATION_EXPORT NSExceptionName const NSPortSendException;
/*
Generic error occurred on receive.
*/
FOUNDATION_EXPORT NSExceptionName const NSPortReceiveException;


/*
No longer used.
*/
FOUNDATION_EXPORT NSExceptionName const NSOldStyleException;

/*
The name of an exception raised by NSArchiver if there are problems initializing or encoding.
*/
FOUNDATION_EXPORT NSExceptionName const NSInconsistentArchiveException;

/***************    Exception object    ***************/

但并不是所有的异常都在这里定义,如UIApplicationInvalidInterfaceOrientation这个异常就是定义在UIKit的UIApplication中的

UIKIT_EXTERN NSExceptionName const UIApplicationInvalidInterfaceOrientationException API_AVAILABLE(ios(6.0)) API_UNAVAILABLE(tvos);

当然我们也可以使用自定义异常进行抛出。

NSString *nilStr = nil;
    NSMutableArray *arrayM = [NSMutableArray array];
    @try {
        //如果@try中的代码会导致程序崩溃,就会来到@catch
        
        //将一个nil插入到可变数组中,这行代码肯定有问题
        [arrayM addObject:nilStr];
    }
    @catch (NSException *exception) {
        //在这里你可以进行相应的处理操作
        //异常的名称
        NSString *exceptionName = @"异常的名称";
        //异常的原因
        NSString *exceptionReason = @"我异常的原因";
        //异常的信息
        NSDictionary *exceptionUserInfo = nil;

        NSException *exception1 = [NSException exceptionWithName:exceptionName reason:exceptionReason userInfo:exceptionUserInfo];

        //抛异常
        @throw exception1;
    }
    @finally {
        //@finally中的代码是一定会执行的
        
        //你可以在这里进行一些相应的操作
    }

热门崩溃

下面我们看一下这些热门崩溃都是什么以及产生的原因

1,NSInvalidArgumentException

非法参数异常(NSInvalidArgumentException)是Objective-C代码最常出现的错误,所以平时写代码的时候,需要多加注意,加强对参数的检查,避免传入非法参数导致异常,其中尤以nil参数为甚。
(1)无法识别选择器

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(100, 100, 100, 100);
    button.backgroundColor = [UIColor redColor];
    // 未实现buttonAction
    [button addTarget:self action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];

(2)数组中插入异常数据,传递nil

NSMutableArray *array = [NSMutableArray array];
[array addObject:nil];

(3)NSString在使用stringWithString时,传递nil

NSString *str = [NSString stringWithString:nil];

(4)参数类型传递错误

UITextField *textField = [[UITextField alloc] init];
    textField.background = [UIColor blueColor];

2,NSGenericException

通用异常
(1)foreach操作
NSGenericException这个异常容易出现在foreach操作中,在for in循环中如果修改所遍历的数组,无论你是add或remove,都会出错

NSArray *array = @[@"111", @"222", @"333", @"444", @"555"];
    NSMutableArray *marray = [array mutableCopy];
    for (NSString *item in marray) {
//        [marray addObject:@"666"];
        if ([item isEqualToString:@"111"]) {
            [marray removeObject:item];
        }
    }

解决办法:如果有add或remove操作请使用for循环。
(2)读取文件失败

3,NSRangeException

越界异常(NSRangeException)是iOS开发中比较常出现的异常
(1)容器越界

NSArray *arry = @[@"111", @"222", @"333"];
    NSString *str = arry[4];

在使用tableview或者collectionview时数据源容器越界
(2)处理数据范围NSRange超过数据本身的长度

NSDictionary *attributes = @{NSFontAttributeName:[UIFont fontWithName:@"PingFangSC-Regular" size:14],
                                 NSForegroundColorAttributeName:[UIColor colorWithRed:0.2 green:0.2 blue:0.188 alpha:1]
                                 };
    NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:@"123456" attributes:attributes];
    NSRange range = {1,8};
    [mutableAttributedString setAttributes:attributes range:range];

解决办法:为了避免NSRangeException的发生,必须传入的下标参数或者NSRange范围进行合法性检查,判断是否在集合数据的范围内,然后再进行相关的处理
(3)KVO被移除多次

[self.titleLabel addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil];
    self.titleLabel.backgroundColor = [UIColor blueColor];
    //第一次remove
    [self.titleLabel removeObserver:self forKeyPath:@"backgroundColor"];
    //第二次remove
    [self.titleLabel removeObserver:self forKeyPath:@"backgroundColor"];

4,NSMallocException

这是内存不足的问题,无法分配足够的内存空间,比如需要分配的内存大小是一个不正常的值,比较巨大或者设备的内存空间不足以及耗尽
(1)分配空间过大

NSMutableData *data = [[NSMutableData alloc] initWithCapacity:1];
NSInteger len = 203293514200000000;
[data increaseLengthBy:len];

(2)图像占用空间过大

-[SDImageCache storeImage:recalculateFromImage:imageData:forKey:toDisk:]

如果imageData长度过长,就会出现NSMallocException
(3)OOM问题

Terminating app due to uncaught exception 'NSMallocException', reason: 'Out of memory. We suggest restarting the application. If you have an unsaved document, create a backup copy in Finder, then try to save

这种情况一般是程序陷入死循环,注意检查代码
解决办法:对于程序中分配内存空间的操作,需要检查参数(空间大小)的有效性,特别是这个参数来自其他模块的返回值,更应该注意。

5,NSInternalInconsistencyException

内部不一致异常(NSInternalInconsistencyException)

(1)NSMutableDictionary的错误使用:比如把NSDictionary当做NSMutableDictionary来使用,从他们内部机理来说,就会产生一些错误,NSMutableDictionary中有很多NSDictionary不支持的接口。

(2)界面使用不当:如在子线程刷新UI

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.tableView.frame = CGRectMake(0, 0, 10, 0); });

解决办法:通过runtime的方法替换,替换UIView 的 setNeedsLayout, layoutIfNeeded,layoutSubviews, setNeedsUpdateConstraints。方法,判断当前线程是否为主线程,如果不是,在主线程执行。

(3)tableview里再cellForRowAtIndexPath方法中,返回的内容不是UITableViewCell类型,比如返回了nil

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { return nil; }

(4)KVO的observer没有实现observeValueForKeyPath方法

[self.titleLabel addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil]; self.titleLabel.backgroundColor = [UIColor blueColor]; // 未实现observeValueForKeyPath

6,UIApplicationInvalidInterfaceOrientation

应用程序无效界面定向异常

(1)ViewController中设置的方位跟应用支持的方位不一致:应用只支持竖屏,VC却支持横屏

苹果目前已经对这种情况做了兼容,如果应用只支持竖屏,而VC支持横屏的情况只会横屏无效果,并不会crash了。

7,CALayerInvalidGeometry

CALayer无效坐标异常

(1)rect里面包含非数字

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.frame = CGRectMake(100, 100, 100, NAN);

(2)rect中在计算时分母为0

苹果目前已经对这种情况做了兼容,如果分母为0则会报一个警告,并且该值当做0来处理

坐标异常

8,NSFilehandleOperationException

手机空间不足,会使客户端直接崩溃,触发NSFilehandleOperationException,所以在处理文件时,比如应用频繁的保存文档,缓存资料或者处理比较大的数据,需要考虑空间的问题

(1)没有空间:手机没有存储空间了,或者需要写的文件太大,会触发“No space left on device”异常

(2)文件读写权限:明明是要写文件,可只给了读权限,所以触发了“Bad file descriptor”异常

(3)读文件失败: - readDataOfLength:

(4)获取文件数据失败

解决办法:在处理文件I/O时,需要考虑到存储空间的有限性,对大小参数进行有效性校验;另外对NSFileHandle的有效性也要判断。

9,NSUnknownKeyException

未知key异常

(1)不符合键值编码

(2)kvc使用了不存在的key

[self.parentVC setValue:@"123" forKey:@"abc"];

10,NSArchiverArchiveInconsistency

存档不一致异常

总结

这些热门崩溃是占比较大的崩溃,可以针对这些常见的、热门的崩溃进行防护,然后再辅助crash上报以及日志等功能来保证App应用的稳定性。

当然除了这些热门崩溃还会有很多偶现的、不好定位的崩溃,需要花更多的精力来解决。

你可能感兴趣的:(iOS热门崩溃分析)