iOS Crash

代碼下載

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日志拿到了,怎么復現(xiàn)該現(xiàn)象呢?我們看到initWithObjects:forKeys:count:,猜測一下應該是NSDictionary初始化時的問題,在看后面的提示attempt to insert nil object,此時就可以做一個猜測,應該是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ù)據(jù)轉換成model,而是通過一個reformer機制轉換成NSDictionary形式,提供給目標對象使用,在轉換成NSDictionary的過程中,后臺返回的數(shù)據(jù)有時可能為空,就會造成插入nil對象,從而導致crash。

有3種方案可以解決該問題,如下:

方案一:后臺在返回數(shù)據(jù)的時候進行校驗,對空值進行處理。但是在項目中有些空值是有特殊的用途,此種方案不可行。

方案二:在轉換成NSDictionary的時候,對后臺返回的數(shù)據(jù)進行校驗,把空值轉換成NSNull對象。方案可行,但是需要對現(xiàn)有代碼做大的改動,每次轉換的時候都需要進行校驗,太麻煩。業(yè)務高速發(fā)展時期,這樣做成本太高。

方案三:有沒有一種無須改動現(xiàn)有代碼又能解決該問題呢?答案是有的,可以利用Objective-C的runtime來解決該問題。

NSDictionary插入nil對象會造成崩潰,但是插入NSNull對象是不會造成崩潰的,只要利用runtime的Swizzle Method把nil對象給轉換成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

當這些對象調(diào)用這幾個不存在的方法的時候,替換成自己定義的一個方法,對它們做一下錯誤兼容,使應用不會崩潰?,F(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

小結一下,造成NSInvalidArgumentException異常大概有以下原因:
NSDictionary插入nil的對象。NSMutableDictionary也是同樣的道理。
NSJSONSerialization序列化的時候,傳入data為nil。
an unrecognized selector 無法識別的方法
NSInvalidArgumentException的崩潰記錄有149條(總崩潰記錄304條),占49.01%,稱霸Crash界,殺手排名第一。

殺手 NO.2:SIGSEGV 異常

SIGSEGV是當SEGV發(fā)生的時候,讓代碼終止的標識。當去訪問沒有被開辟的內(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)存泄漏,應該在其不使用的時候加上如下代碼:
free(dataOut);
另外,通過這個例子我們也要特別注意,在使用C語言對象的時候,一定要記得在不使用的時候給釋放掉,ARC并不能釋放掉這塊內(nèi)存。
例子2:
Can't add self as subview crash
造成這個崩潰的原因,一種原因是在push或pop一個視圖的時候,并且設置了animated:YES,如果此時動畫(animated)還沒有完成,這個時候,你在去push或pop另外一個視圖的時候,就會造成該異常。 也有其他原因可以造成這個崩潰,比如:
[self.view addSubview:self.view];
復現(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設置為NO,但是很不友好,把系統(tǒng)自帶的動畫效果給去掉了。另外一種友好的方式就是通過runtime來進行實現(xiàn)了,通過安全的方式,確保當有控制器正在進行入?;虺鰲r,沒有其他入?;虺鰲2僮?。具體源碼如下:


#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 {
        //添加失?。赫f明源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)

// 相當于為分類添加屬性viewTransitionInProgress的set賦值方法
- (void)setViewTransitionInProgress:(BOOL)property {
    NSNumber *number = [NSNumber numberWithBool:property];
    // 相當于賦值,為key ObjectTagKey 賦值一個value即number,因為key是固定的,因此下面的取值是可行的
    objc_setAssociatedObject(self, ObjectTagKey, number , OBJC_ASSOCIATION_RETAIN);
}

// 相當于為分類添加屬性viewTransitionInProgress的get取值方法
- (BOOL)isViewTransitionInProgress {

    // 相當于從字典的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

通過日志可以很明顯的知道問題,就是越界造成的,復現(xiàn)該現(xiàn)象也比較簡單,在此就略過了。怎么解決呢?
方案一:
在對數(shù)組取數(shù)據(jù)的時候,要判斷一下數(shù)組的長度大于取的index,這個要在平時寫代碼的時候給規(guī)范起來。同樣在對字符串進行截取的時候,也需要做類似的判斷。但現(xiàn)實的情況是,有時我們會忘了寫這樣的邏輯判斷,就會有潛在的崩潰問題。如何做一下統(tǒng)一的判斷呢?即使開發(fā)人員忘了寫這樣的邏輯判斷也不會造成崩潰,從框架層面來杜絕這類的崩潰,方案二給出了答案。
方案二:
利用runtime的Swizzle Method特性,可以實現(xiàn)從框架層面杜絕這類的崩潰問題,這樣做的好處有兩點:
1.開發(fā)人員忘了寫判斷越界的邏輯,也不會造成app的崩潰,對開發(fā)人員來說是透明的。
2.不需要修改現(xiàn)有的代碼,對現(xiàn)有代碼的侵入性降低到最低,不需要添加大量重復的邏輯判斷代碼。
全部源碼會在文章末尾提供,現(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)關閉的socket調(diào)用兩次write,第二次write將會產(chǎn)生SIGPIPE信號,該信號默認結束進程。
那如何解決該問題呢?對SIGPIPE信號可以進行捕獲,也可將其忽略,對于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)關閉的socket調(diào)用兩次write,第二次write將會產(chǎn)生SIGPIPE信號,該信號默認結束進程。
那如何解決該問題呢?對SIGPIPE信號可以進行捕獲,也可將其忽略,對于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 異常

這是一個讓程序終止的標識,會在斷言、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界排名第五。

殺手總結
前面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/

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • [這是第14篇] 序: iOS Crash問題是iOS開發(fā)中難以忽視的存在,本文就捕獲iOS Crash、Cras...
    南華coder閱讀 10,097評論 21 116
  • 本文就捕獲iOS Crash、Crash日志組成、Crash日志符號化、異常信息解讀、常見的Crash五部分介紹。...
    xukuangbo_閱讀 1,728評論 0 0
  • 2017-10-12,星期四,晴,今天女兒放學回家我在做飯,給孩子們炒了愛吃的米粉,熬了玉米粥,炒了個小白菜豆腐,...
    文慧成方媽媽閱讀 289評論 0 0
  • 各種絮絮叨叨 魏:你是不是不會說的也會用普通話代替啊? 沙:對啊。嗯沒有是因為,嗯,有些家鄉(xiāng)話不知道怎么說嘛。 魏...
    懷夢草閱讀 211評論 3 1
  • 陽光 在風中把光芒揮灑 一行熟悉的腳印 清晰的刻在眼簾 花開時,看蜂蝶的飛舞 花兒落了,凄涼在蔓延 就這樣看著 時...
    喂馬砍柴四海為家閱讀 145評論 0 3

友情鏈接更多精彩內(nèi)容