前言
隨著移動(dòng)互聯(lián)網(wǎng)的發(fā)展,各種App應(yīng)用已經(jīng)融入到我們的日常生活當(dāng)中,應(yīng)用的穩(wěn)定性的要求也越來越高,首當(dāng)其沖的就是應(yīng)用的crash問題,輕則影響用戶的良好體驗(yàn),重則導(dǎo)致用戶大量流失造成巨大的影響。所以解決crash是重要而緊急的事情。
2021年友盟+發(fā)布了移動(dòng)應(yīng)用性能體驗(yàn)報(bào)告,報(bào)告中指出App的整理崩潰率為0.29%,其中iOS端崩潰率為0.10%。以及熱門崩潰排行榜:

本文將通過熱門崩潰產(chǎn)生的原因以及崩潰demo(寫bug)來了解這些崩潰是如何產(chǎn)生的。熱門崩潰占比是很高的,如果能解決這些熱門崩潰,移動(dòng)應(yīng)用的質(zhì)量會(huì)有很大的提升。
NSException
首先我們先來介紹一下NSException:

相信大家對(duì)這個(gè)頁面不會(huì)陌生吧,這個(gè)日志就是NSException產(chǎn)生的,一旦程序拋出異常,程序就會(huì)崩潰,控制臺(tái)就會(huì)輸出這些崩潰日志。
NSException對(duì)象繼承自NSObject,是專門用來拋出Objective-C異常的,有四個(gè)屬性:
- name:異常名稱
- reason:異常原因
- userInfo:異常信息,字典形式
- reserved:堆棧信息
@interface NSException : NSObject <NSCopying, NSSecureCoding> {
@private
NSString *name;
NSString *reason;
NSDictionary *userInfo;
id reserved;
}
當(dāng)出現(xiàn)異常時(shí),會(huì)拋出一個(gè)NSException對(duì)象,內(nèi)容如上圖所示。

除了上面的屬性外,NSException還預(yù)定義了一些通用的異常名稱:
/*************** 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這個(gè)異常就是定義在UIKit的UIApplication中的
UIKIT_EXTERN NSExceptionName const UIApplicationInvalidInterfaceOrientationException API_AVAILABLE(ios(6.0)) API_UNAVAILABLE(tvos);
當(dāng)然我們也可以使用自定義異常進(jìn)行拋出。
NSString *nilStr = nil;
NSMutableArray *arrayM = [NSMutableArray array];
@try {
//如果@try中的代碼會(huì)導(dǎo)致程序崩潰,就會(huì)來到@catch
//將一個(gè)nil插入到可變數(shù)組中,這行代碼肯定有問題
[arrayM addObject:nilStr];
}
@catch (NSException *exception) {
//在這里你可以進(jìn)行相應(yīng)的處理操作
//異常的名稱
NSString *exceptionName = @"異常的名稱";
//異常的原因
NSString *exceptionReason = @"我異常的原因";
//異常的信息
NSDictionary *exceptionUserInfo = nil;
NSException *exception1 = [NSException exceptionWithName:exceptionName reason:exceptionReason userInfo:exceptionUserInfo];
//拋異常
@throw exception1;
}
@finally {
//@finally中的代碼是一定會(huì)執(zhí)行的
//你可以在這里進(jìn)行一些相應(yīng)的操作
}
熱門崩潰
下面我們看一下這些熱門崩潰都是什么以及產(chǎn)生的原因
1,NSInvalidArgumentException
非法參數(shù)異常(NSInvalidArgumentException)是Objective-C代碼最常出現(xiàn)的錯(cuò)誤,所以平時(shí)寫代碼的時(shí)候,需要多加注意,加強(qiáng)對(duì)參數(shù)的檢查,避免傳入非法參數(shù)導(dǎo)致異常,其中尤以nil參數(shù)為甚。
(1)無法識(shí)別選擇器
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(100, 100, 100, 100);
button.backgroundColor = [UIColor redColor];
// 未實(shí)現(xiàn)buttonAction
[button addTarget:self action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
(2)數(shù)組中插入異常數(shù)據(jù),傳遞nil
NSMutableArray *array = [NSMutableArray array];
[array addObject:nil];
(3)NSString在使用stringWithString時(shí),傳遞nil
NSString *str = [NSString stringWithString:nil];
(4)參數(shù)類型傳遞錯(cuò)誤
UITextField *textField = [[UITextField alloc] init];
textField.background = [UIColor blueColor];
2,NSGenericException
通用異常
(1)foreach操作
NSGenericException這個(gè)異常容易出現(xiàn)在foreach操作中,在for in循環(huán)中如果修改所遍歷的數(shù)組,無論你是add或remove,都會(huì)出錯(cuò)
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操作請(qǐng)使用for循環(huán)。
(2)讀取文件失敗
3,NSRangeException
越界異常(NSRangeException)是iOS開發(fā)中比較常出現(xiàn)的異常
(1)容器越界
NSArray *arry = @[@"111", @"222", @"333"];
NSString *str = arry[4];
在使用tableview或者collectionview時(shí)數(shù)據(jù)源容器越界
(2)處理數(shù)據(jù)范圍NSRange超過數(shù)據(jù)本身的長(zhǎng)度
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的發(fā)生,必須傳入的下標(biāo)參數(shù)或者NSRange范圍進(jìn)行合法性檢查,判斷是否在集合數(shù)據(jù)的范圍內(nèi),然后再進(jìn)行相關(guān)的處理
(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
這是內(nèi)存不足的問題,無法分配足夠的內(nèi)存空間,比如需要分配的內(nèi)存大小是一個(gè)不正常的值,比較巨大或者設(shè)備的內(nèi)存空間不足以及耗盡
(1)分配空間過大
NSMutableData *data = [[NSMutableData alloc] initWithCapacity:1];
NSInteger len = 203293514200000000;
[data increaseLengthBy:len];
(2)圖像占用空間過大
-[SDImageCache storeImage:recalculateFromImage:imageData:forKey:toDisk:]
如果imageData長(zhǎng)度過長(zhǎng),就會(huì)出現(xiàn)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
這種情況一般是程序陷入死循環(huán),注意檢查代碼
解決辦法:對(duì)于程序中分配內(nèi)存空間的操作,需要檢查參數(shù)(空間大?。┑挠行?,特別是這個(gè)參數(shù)來自其他模塊的返回值,更應(yīng)該注意。
5,NSInternalInconsistencyException
內(nèi)部不一致異常(NSInternalInconsistencyException)
(1)NSMutableDictionary的錯(cuò)誤使用:比如把NSDictionary當(dāng)做NSMutableDictionary來使用,從他們內(nèi)部機(jī)理來說,就會(huì)產(chǎn)生一些錯(cuò)誤,NSMutableDictionary中有很多NSDictionary不支持的接口。
(2)界面使用不當(dāng):如在子線程刷新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。方法,判斷當(dāng)前線程是否為主線程,如果不是,在主線程執(zhí)行。
(3)tableview里再cellForRowAtIndexPath方法中,返回的內(nèi)容不是UITableViewCell類型,比如返回了nil
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { return nil; }
(4)KVO的observer沒有實(shí)現(xiàn)observeValueForKeyPath方法
[self.titleLabel addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew context:nil]; self.titleLabel.backgroundColor = [UIColor blueColor]; // 未實(shí)現(xiàn)observeValueForKeyPath
6,UIApplicationInvalidInterfaceOrientation
應(yīng)用程序無效界面定向異常
(1)ViewController中設(shè)置的方位跟應(yīng)用支持的方位不一致:應(yīng)用只支持豎屏,VC卻支持橫屏
蘋果目前已經(jīng)對(duì)這種情況做了兼容,如果應(yīng)用只支持豎屏,而VC支持橫屏的情況只會(huì)橫屏無效果,并不會(huì)crash了。
7,CALayerInvalidGeometry
CALayer無效坐標(biāo)異常
(1)rect里面包含非數(shù)字
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.frame = CGRectMake(100, 100, 100, NAN);
(2)rect中在計(jì)算時(shí)分母為0
蘋果目前已經(jīng)對(duì)這種情況做了兼容,如果分母為0則會(huì)報(bào)一個(gè)警告,并且該值當(dāng)做0來處理

8,NSFilehandleOperationException
手機(jī)空間不足,會(huì)使客戶端直接崩潰,觸發(fā)NSFilehandleOperationException,所以在處理文件時(shí),比如應(yīng)用頻繁的保存文檔,緩存資料或者處理比較大的數(shù)據(jù),需要考慮空間的問題
(1)沒有空間:手機(jī)沒有存儲(chǔ)空間了,或者需要寫的文件太大,會(huì)觸發(fā)“No space left on device”異常
(2)文件讀寫權(quán)限:明明是要寫文件,可只給了讀權(quán)限,所以觸發(fā)了“Bad file descriptor”異常
(3)讀文件失?。?- readDataOfLength:
(4)獲取文件數(shù)據(jù)失敗
解決辦法:在處理文件I/O時(shí),需要考慮到存儲(chǔ)空間的有限性,對(duì)大小參數(shù)進(jìn)行有效性校驗(yàn);另外對(duì)NSFileHandle的有效性也要判斷。
9,NSUnknownKeyException
未知key異常
(1)不符合鍵值編碼
(2)kvc使用了不存在的key
[self.parentVC setValue:@"123" forKey:@"abc"];
10,NSArchiverArchiveInconsistency
存檔不一致異常
總結(jié)
這些熱門崩潰是占比較大的崩潰,可以針對(duì)這些常見的、熱門的崩潰進(jìn)行防護(hù),然后再輔助crash上報(bào)以及日志等功能來保證App應(yīng)用的穩(wěn)定性。
當(dāng)然除了這些熱門崩潰還會(huì)有很多偶現(xiàn)的、不好定位的崩潰,需要花更多的精力來解決。