iOS Crash 殺手排名
殺手 NO.1
NSInvalidArgumentException 異常
出現(xiàn)這個crash的原因有很多政溃,選取了崩潰次數(shù)較多的crash矿酵。
crash 日志1-1
-[__NSPlaceholderDictionary initWithObjects:forKeys:count:]:
attempt to insert nil object from objects[3]
crash日志拿到了,怎么復(fù)現(xiàn)該現(xiàn)象呢?我們看到initWithObjects:forKeys:count:浩村,猜測一下應(yīng)該是NSDictionary初始化時的問題证舟,在看后面的提示attempt to insert nil object,此時就可以做一個猜測争舞,應(yīng)該是NSDictionary初始化時插入nil對象造成的異常。下面我們寫一段代碼來驗證一下:
NSString *password = nil;
NSDictionary *dict = @{
@"userName": @"bruce",
@"password": password
};
NSLog(@"dict is : %@", dict);
運行過后澈灼,崩潰信息如下:
2017-01-23 23:08:15.056 CrashDemo[8592:599699] *** Terminating app due
to uncaught exception 'NSInvalidArgumentException', reason: '***
-[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert
nil object from objects[1]'
上面的崩潰信息證明了我們的猜測竞川。從崩潰日志記錄中,查詢到該問題的崩潰記錄有33條(總崩潰記錄304條)叁熔,占10.85%委乌,崩潰率比較高。為什么會出現(xiàn)這種現(xiàn)象呢荣回?如何解決這樣的crash呢遭贸?
崩潰率高的原因是因為自己的框架中采用了去model化的設(shè)計思想,不會把后臺返回的數(shù)據(jù)轉(zhuǎn)換成model驹马,而是通過一個reformer機制轉(zhuǎn)換成NSDictionary形式革砸,提供給目標(biāo)對象使用,在轉(zhuǎn)換成NSDictionary的過程中糯累,后臺返回的數(shù)據(jù)有時可能為空算利,就會造成插入nil對象,從而導(dǎo)致crash泳姐。
有3種方案可以解決該問題效拭,如下:
方案一:后臺在返回數(shù)據(jù)的時候進(jìn)行校驗,對空值進(jìn)行處理胖秒。但是在項目中有些空值是有特殊的用途缎患,此種方案不可行。
方案二:在轉(zhuǎn)換成NSDictionary的時候阎肝,對后臺返回的數(shù)據(jù)進(jìn)行校驗挤渔,把空值轉(zhuǎn)換成NSNull對象。方案可行风题,但是需要對現(xiàn)有代碼做大的改動判导,每次轉(zhuǎn)換的時候都需要進(jìn)行校驗嫉父,太麻煩。業(yè)務(wù)高速發(fā)展時期眼刃,這樣做成本太高绕辖。
方案三:有沒有一種無須改動現(xiàn)有代碼又能解決該問題呢?答案是有的擂红,可以利用Objective-C的runtime來解決該問題仪际。
NSDictionary插入nil對象會造成崩潰,但是插入NSNull對象是不會造成崩潰的昵骤,只要利用runtime的Swizzle Method把nil對象給轉(zhuǎn)換成NSNull對象就可以把該問題給解決了树碱。創(chuàng)建一個NSDictionary的類別,利用runtime的Swizzle Method來替換系統(tǒng)的方法变秦。源碼實現(xiàn)可以參考Glow團隊封裝的NSDictionary+NilSafe(Github上可下載到), 全部源碼會在文章末尾提供赴恨,現(xiàn)截取其中的部分代碼如下:
- (void)gl_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
if (!aKey) {
return;
}
if (!anObject) {
anObject = [NSNull null];
}
[self gl_setObject:anObject forKey:aKey];
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = NSClassFromString(@"__NSDictionaryM");
[class gl_swizzleMethod:@selector(setObject:forKey:) withMethod:@selector(gl_setObject:forKey:)];
[class gl_swizzleMethod:@selector(setObject:forKeyedSubscript:) withMethod:@selector(gl_setObject:forKeyedSubscript:)];
});
}
crash 日志1-2
2017-01-25 11:33:17.541 CrashDemo[4025:252254] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'data parameter is nil'
通過日志信息,可以把崩潰問題定位到參數(shù)為nil的情況伴栓,在看了下堆棧的日志信息,把問題定位到了NSJSONSerialization序列化的時候雨饺,傳入data為nil钳垮,造成的崩潰。為了驗證是不是該問題额港,我寫了一段代碼做了下驗證:
NSData *data = nil;
NSError *error;
NSDictionary *orginDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
NSLog(@"originDict is : %@", orginDict);
這個問題比較好解決饺窿,在序列化的時候,統(tǒng)一加入判斷移斩,判斷data是不是nil即可
crash 日志1-3
unrecognized selector sent to instance 0x15d23910
造成這條崩潰的原因肚医,想必大家都比較熟悉了,就是一個類調(diào)用了一個不存在的方法向瓷,造成的崩潰肠套。解決這樣的問題,可以在寫一個方法的時候猖任,判斷一下其類的類型你稚,不符合類型的不讓其調(diào)用,也可以使用runtime對常見的方法調(diào)用做一下錯誤兼容朱躺。比如我這邊經(jīng)常會出現(xiàn)這樣的崩潰
-[__NSCFConstantString objectForKeyedSubscript:]: unrecognized selector sent to instance 0x1741af420
-[NSNull length]: unrecognized selector sent to instance 0x1b21e6ef8
-[__NSCFConstantString objectForKeyedSubscript:]: unrecognized selector sent to instance
-[__NSDictionaryI length]: unrecognized selector sent to instance 0x174264500
當(dāng)這些對象調(diào)用這幾個不存在的方法的時候刁赖,替換成自己定義的一個方法,對它們做一下錯誤兼容长搀,使應(yīng)用不會崩潰∮畛冢現(xiàn)截取部分代碼實現(xiàn),全部源碼會在文章末尾提供源请。
@implementation NSString (NSRangeException)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
[objc_getClass("__NSCFConstantString") swizzleMethod:@selector(objectForKeyedSubscript:) swizzledSelector:@selector(replace_objectForKeyedSubscript:)];
}
});
}
- (id)replace_objectForKeyedSubscript:(NSString *)key {
return nil;
}
@end
小結(jié)一下枪芒,造成NSInvalidArgumentException異常大概有以下原因:
NSDictionary插入nil的對象彻况。NSMutableDictionary也是同樣的道理。
NSJSONSerialization序列化的時候病苗,傳入data為nil疗垛。
an unrecognized selector 無法識別的方法
NSInvalidArgumentException的崩潰記錄有149條(總崩潰記錄304條),占49.01%硫朦,稱霸Crash界贷腕,殺手排名第一。
殺手 NO.2:SIGSEGV 異常
SIGSEGV是當(dāng)SEGV發(fā)生的時候咬展,讓代碼終止的標(biāo)識泽裳。當(dāng)去訪問沒有被開辟的內(nèi)存或者已經(jīng)被釋放的內(nèi)存時,就會發(fā)生這樣的異常破婆。另外涮总,在低內(nèi)存的時候,也可能會產(chǎn)生這樣的異常祷舀。
對于這樣的異常瀑梗,我們可以使用兩種方式來解決,一種方式使用Xcode自帶的內(nèi)存分析工具(Leaks)裳扯,一種是使用facebook提供的自動化工具來監(jiān)測內(nèi)存泄漏問題抛丽,如:
FBRetainCycleDetector、FBAllocationTracker饰豺、FBMemoryProfiler
例子1:
dataOut = malloc(dataOutAvailable * sizeof(uint8_t));
這是使用Xcode自帶的Leaks工具檢測到的內(nèi)存泄漏亿鲜,通過代碼我們看出這是一個C語言使用malloc函數(shù)分配了一塊內(nèi)存地址,但是在不使用的時候卻忘記了釋放其內(nèi)存地址冤吨,這樣就造成了內(nèi)存泄漏蒿柳,應(yīng)該在其不使用的時候加上如下代碼:
free(dataOut);
另外,通過這個例子我們也要特別注意漩蟆,在使用C語言對象的時候垒探,一定要記得在不使用的時候給釋放掉,ARC并不能釋放掉這塊內(nèi)存爆安。
例子2:
Can't add self as subview crash
造成這個崩潰的原因叛复,一種原因是在push或pop一個視圖的時候,并且設(shè)置了animated:YES扔仓,如果此時動畫(animated)還沒有完成褐奥,這個時候,你在去push或pop另外一個視圖的時候翘簇,就會造成該異常撬码。 也有其他原因可以造成這個崩潰,比如:
[self.view addSubview:self.view];
復(fù)現(xiàn)這個現(xiàn)象版保,我寫了一個下面的代碼測試呜笑,如下:
- (IBAction)btnAction:(id)sender {
UIViewController *test01 = [[UIViewController alloc] init];
[self.navigationController pushViewController:test01 animated:YES];
[self.navigationController pushViewController:test01 animated:YES];
}
解決該異常最簡單的方式是把animated設(shè)置為NO夫否,但是很不友好,把系統(tǒng)自帶的動畫效果給去掉了叫胁。另外一種友好的方式就是通過runtime來進(jìn)行實現(xiàn)了凰慈,通過安全的方式,確保當(dāng)有控制器正在進(jìn)行入椡斩欤或出棧時微谓,沒有其他入棧或出棧操作输钩。具體源碼如下:
#import <Foundation/Foundation.h>
@interface NSObject (Swizzling)
+ (void)swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;
@end
#import "NSObject+Swizzling.h"
#import <objc/runtime.h>
@implementation NSObject (Swizzling)
//交互方法的實現(xiàn)
+ (void)swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector {
Class class = [self class];
//原有方法
Method originalMethod = class_getInstanceMethod(class, originalSelector);
//替換原有方法的新方法
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//先嘗試給源SEL添加IMP豺型,這里是為了避免源SEL沒有實現(xiàn)IMP的情況
BOOL didAddMethod = class_addMethod(class,originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
//添加成功:說明源SEL沒有實現(xiàn)IMP,將源SEL的IMP替換到交換SEL的IMP
class_replaceMethod(class,swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
//添加失斅蚰恕:說明源SEL已經(jīng)有IMP姻氨,直接將兩個SEL的IMP交換即可
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
下面代碼中引用的 + (void)swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;方法便是上面的NSObject的分類中新增的方法
#import <UIKit/UIKit.h>
@interface UINavigationController (Consistent)
@end
#import "UINavigationController+Consistent.h"
#import <objc/runtime.h>
/// This char is used to add storage for the isPushingViewController property.
static char const * const ObjectTagKey = "ObjectTag";
@interface UINavigationController () <UINavigationControllerDelegate>
@property (readwrite,getter = isViewTransitionInProgress) BOOL viewTransitionInProgress;
@end
@implementation UINavigationController (Consistent)
// 相當(dāng)于為分類添加屬性viewTransitionInProgress的set賦值方法
- (void)setViewTransitionInProgress:(BOOL)property {
NSNumber *number = [NSNumber numberWithBool:property];
// 相當(dāng)于賦值,為key ObjectTagKey 賦值一個value即number,因為key是固定的,因此下面的取值是可行的
objc_setAssociatedObject(self, ObjectTagKey, number , OBJC_ASSOCIATION_RETAIN);
}
// 相當(dāng)于為分類添加屬性viewTransitionInProgress的get取值方法
- (BOOL)isViewTransitionInProgress {
// 相當(dāng)于從字典的key ObjectTagKey 中獲得一個值,只不過這個字典又沒有顯性的表現(xiàn)出來
NSNumber *number = objc_getAssociatedObject(self, ObjectTagKey);
return [number boolValue];
}
+ (void)load {
//-- Exchange the original implementation with our custom one.
method_exchangeImplementations(class_getInstanceMethod(self, @selector(pushViewController:animated:)), class_getInstanceMethod(self, @selector(safePushViewController:animated:)));
}
- (void)safePushViewController:(UIViewController *)viewController animated:(BOOL)animated {
self.delegate = self;
//-- If we are already pushing a view controller, we dont push another one.
if (self.isViewTransitionInProgress == NO) {
//-- This is not a recursion, due to method swizzling the call below calls the original method.
[self safePushViewController:viewController animated:animated];
if (animated) {
self.viewTransitionInProgress = YES;
}
}
}
SIGSEGV的崩潰記錄有57條(總共304條崩潰記錄),占18.75%剪验。在Crash界排名第二肴焊。
殺手 NO.3:NSRangeException 異常
造成這個異常,就是越界異常了功戚,在iOS中我們經(jīng)常碰到的越界異常有兩種抖韩,一種是數(shù)組越界,一種字符串截取越界疫铜,我們通過crash日志來具體分析一下。
crash 日志3-1
-[__NSArrayM objectAtIndex:]: index 1 beyond bounds for empty array
-[__NSCFConstantString substringToIndex:]: Index 10 out of bounds; string length 0
通過日志可以很明顯的知道問題双谆,就是越界造成的壳咕,復(fù)現(xiàn)該現(xiàn)象也比較簡單,在此就略過了顽馋。怎么解決呢谓厘?
方案一:
在對數(shù)組取數(shù)據(jù)的時候,要判斷一下數(shù)組的長度大于取的index寸谜,這個要在平時寫代碼的時候給規(guī)范起來竟稳。同樣在對字符串進(jìn)行截取的時候,也需要做類似的判斷熊痴。但現(xiàn)實的情況是他爸,有時我們會忘了寫這樣的邏輯判斷,就會有潛在的崩潰問題果善。如何做一下統(tǒng)一的判斷呢诊笤?即使開發(fā)人員忘了寫這樣的邏輯判斷也不會造成崩潰,從框架層面來杜絕這類的崩潰巾陕,方案二給出了答案讨跟。
方案二:
利用runtime的Swizzle Method特性纪他,可以實現(xiàn)從框架層面杜絕這類的崩潰問題,這樣做的好處有兩點:
1.開發(fā)人員忘了寫判斷越界的邏輯晾匠,也不會造成app的崩潰茶袒,對開發(fā)人員來說是透明的。
2.不需要修改現(xiàn)有的代碼凉馆,對現(xiàn)有代碼的侵入性降低到最低薪寓,不需要添加大量重復(fù)的邏輯判斷代碼。
全部源碼會在文章末尾提供句喜,現(xiàn)截取部分代碼實現(xiàn):
@implementation NSArray (NSRangeException)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
[objc_getClass("__NSArray0") swizzleMethod:@selector(objectAtIndex:) swizzledSelector:@selector(emptyObjectIndex:)];
[objc_getClass("__NSArrayI") swizzleMethod:@selector(objectAtIndex:) swizzledSelector:@selector(arrObjectIndex:)];
[objc_getClass("__NSArrayM") swizzleMethod:@selector(objectAtIndex:) swizzledSelector:@selector(mutableObjectIndex:)];
[objc_getClass("__NSArrayM") swizzleMethod:@selector(insertObject:atIndex:) swizzledSelector:@selector(mutableInsertObject:atIndex:)];
}
});
}
- (id)emptyObjectIndex:(NSInteger)index{
return nil;
}
- (id)arrObjectIndex:(NSInteger)index{
if (index >= self.count || index < 0) {
return nil;
}
return [self arrObjectIndex:index];
}
- (id)mutableObjectIndex:(NSInteger)index{
if (index >= self.count || index < 0) {
return nil;
}
return [self mutableObjectIndex:index];
}
- (void)mutableInsertObject:(id)object atIndex:(NSUInteger)index{
if (object) {
[self mutableInsertObject:object atIndex:index];
}
}
@end
**越界的崩潰記錄有46條(總共崩潰記錄是304條)预愤,占15.13%,在crash界殺手排名第三咳胃。
**
殺手 NO.4:SIGPIPE 異常
先解釋一下什么是SIGPIPE異常植康,通俗一點的描述是這樣的:對一個端已經(jīng)關(guān)閉的socket調(diào)用兩次write,第二次write將會產(chǎn)生SIGPIPE信號展懈,該信號默認(rèn)結(jié)束進(jìn)程销睁。
那如何解決該問題呢?對SIGPIPE信號可以進(jìn)行捕獲存崖,也可將其忽略冻记,對于iOS系統(tǒng)來說,只需要把下面這段代碼放在.pch文件中即可来惧。
// 僅在 IOS 系統(tǒng)上支持 SO_NOSIGPIPE
#if defined(SO_NOSIGPIPE) && !defined(MSG_NOSIGNAL)
// We do not want SIGPIPE if writing to socket.
const int value = 1;
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(int));
#endif
SIGPIPE的崩潰記錄有11條(總共304條崩潰記錄)冗栗,占3.61%。在Crash界排名第四
先解釋一下什么是SIGPIPE異常供搀,通俗一點的描述是這樣的:對一個端已經(jīng)關(guān)閉的socket調(diào)用兩次write隅居,第二次write將會產(chǎn)生SIGPIPE信號,該信號默認(rèn)結(jié)束進(jìn)程葛虐。
那如何解決該問題呢胎源?對SIGPIPE信號可以進(jìn)行捕獲,也可將其忽略屿脐,對于iOS系統(tǒng)來說涕蚤,只需要把下面這段代碼放在.pch文件中即可。
// 僅在 IOS 系統(tǒng)上支持 SO_NOSIGPIPE
#if defined(SO_NOSIGPIPE) && !defined(MSG_NOSIGNAL)
// We do not want SIGPIPE if writing to socket.
const int value = 1;
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(int));
#endif
SIGPIPE的崩潰記錄有11條(總共304條崩潰記錄)的诵,占3.61%万栅。在Crash界排名第四。
殺手 NO.5:SIGABRT 異常
這是一個讓程序終止的標(biāo)識西疤,會在斷言申钩、app內(nèi)部、操作系統(tǒng)用終止方法拋出瘪阁。通常發(fā)生在異步執(zhí)行系統(tǒng)方法的時候撒遣。如CoreData邮偎、NSUserDefaults等,還有一些其他的系統(tǒng)多線程操作义黎。
注意:這并不一定意味著是系統(tǒng)代碼存在bug禾进,代碼僅僅是成了無效狀態(tài),或者異常狀態(tài)廉涕。
SIGABRT崩潰記錄9條(總共304條崩潰記錄)泻云,占2.96%。Crash界排名第五狐蜕。
殺手總結(jié)
前面5大crash殺手宠纯,占了89.46%的崩潰率,解決了這5大crash殺手层释,基本上你的app就很健壯了婆瓜,剩下的崩潰問題就需要具體問題具體分析了。
http://zhijianshusheng.github.io/2016/07/11/%E6%8C%89%E5%91%A8%E5%88%86%E7%B1%BB/20160711-0718/%E5%AF%BC%E8%87%B4iOS%E5%B4%A9%E6%BA%83%E7%9A%84%E6%9C%80%E5%B8%B8%E8%A7%815%E5%A4%A7%E5%85%83%E5%87%B6/
https://code.facebook.com/posts/583946315094347/automatic-memory-leak-detection-on-ios/ http://tech.glowing.com/cn/how-we-made-nsdictionary-nil-safe/
http://stackoverflow.com/questions/19560198/ios-app-error-cant-add-self-as-subview https://my.oschina.net/moooofly/blog/474604
http://devma.cn/blog/2016/11/10/ios-beng-kui-crash-jie-xi/