你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS Crash 殺手排名

iOS Crash 殺手排名

編輯:IOS開發基礎

02.jpg

原文

隨著公司嘟嘟牛app用戶數量多了起來,崩潰的問題也多了起來,最近這幾天終於得空,集中時間處理了一下崩潰的問題,現總結一下,希望對大家有所幫助。

殺手 NO.1:NSInvalidArgumentException 異常

出現這個crash的原因有很多,選取了崩潰次數較多的crash。

crash 日志1-1

-[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[3]

crash日志拿到了,怎麼復現該現象呢?我們看到initWithObjects:forKeys:count:,猜測一下應該是NSDictionary初始化時的問題,在看後面的提示attempt to insert nil object,此時就可以做一個猜測,應該是NSDictionary初始化時插入nil對象造成的異常。下面我們寫一段代碼來驗證一下:

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

運行過後,崩潰信息如下:

2.png

Crash 日志1-1

上面的崩潰信息證明了我們的猜測。從崩潰日志記錄中,查詢到該問題的崩潰記錄有33條(總崩潰記錄304條),占10.85%,崩潰率比較高。為什麼會出現這種現象呢?如何解決這樣的crash呢?

崩潰率高的原因是因為自己的框架中采用了去model化的設計思想,不會把後台返回的數據轉換成model,而是通過一個reformer機制轉換成NSDictionary形式,提供給目標對象使用,在轉換成NSDictionary的過程中,後台返回的數據有時可能為空,就會造成插入nil對象,從而導致crash。

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

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

方案二:在轉換成NSDictionary的時候,對後台返回的數據進行校驗,把空值轉換成NSNull對象。方案可行,但是需要對現有代碼做大的改動,每次轉換的時候都需要進行校驗,太麻煩。業務高速發展時期,這樣做成本太高。

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

NSDictionary插入nil對象會造成崩潰,但是插入NSNull對象是不會造成崩潰的,只要利用runtime的Swizzle Method把nil對象給轉換成NSNull對象就可以把該問題給解決了。創建一個NSDictionary的類別,利用runtime的Swizzle Method來替換系統的方法。源碼實現可以參考Glow團隊封裝的NSDictionary+NilSafe(Github上可下載到), 全部源碼會在文章末尾提供,現截取其中的部分代碼如下:

+ (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];
}

crash 日志1-2

data parameter is nil

通過日志信息,可以把崩潰問題定位到參數為nil的情況,在看了下堆棧的日志信息,把問題定位到了NSJSONSerialization序列化的時候,傳入data為nil,造成的崩潰。為了驗證是不是該問題,我寫了一段代碼做了下驗證:

NSData *data = nil;
NSError *error;
NSDictionary *orginDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
NSLog(@"originDict is : %@", orginDict);

運行後,崩潰信息如下:

1.png

Crash日志 1-2

這個問題比較好解決,在序列化的時候,統一加入判斷,判斷data是不是nil即可。

crash 日志1-3

unrecognized selector sent to instance 0x15d23910

造成這條崩潰的原因,想必大家都比較熟悉了,就是一個類調用了一個不存在的方法,造成的崩潰。解決這樣的問題,可以在寫一個方法的時候,判斷一下其類的類型,不符合類型的不讓其調用,也可以使用runtime對常見的方法調用做一下錯誤兼容。比如我這邊經常會出現這樣的崩潰:

-[__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

當這些對象調用這幾個不存在的方法的時候,替換成自己定義的一個方法,對它們做一下錯誤兼容,使應用不會崩潰。現截取部分代碼實現,全部源碼會在文章末尾提供。

@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發生的時候,讓代碼終止的標識。當去訪問沒有被開辟的內存或者已經被釋放的內存時,就會發生這樣的異常。另外,在低內存的時候,也可能會產生這樣的異常。

對於這樣的異常,我們可以使用兩種方式來解決,一種方式使用Xcode自帶的內存分析工具(Leaks),一種是使用facebook提供的自動化工具來監測內存洩漏問題,如:

FBRetainCycleDetector、FBAllocationTracker、FBMemoryProfiler

例子1:

dataOut = malloc(dataOutAvailable * sizeof(uint8_t));

這是使用Xcode自帶的Leaks工具檢測到的內存洩漏,通過代碼我們看出這是一個C語言使用malloc函數分配了一塊內存地址,但是在不使用的時候卻忘記了釋放其內存地址,這樣就造成了內存洩漏,應該在其不使用的時候加上如下代碼:

free(dataOut);

另外,通過這個例子我們也要特別注意,在使用C語言對象的時候,一定要記得在不使用的時候給釋放掉,ARC並不能釋放掉這塊內存。

例子2:

Can't add self as subview crash

造成這個崩潰的原因,一種原因是在push或pop一個視圖的時候,並且設置了animated:YES,如果此時動畫(animated)還沒有完成,這個時候,你在去push或pop另外一個視圖的時候,就會造成該異常。 也有其他原因可以造成這個崩潰,比如:

[self.view addSubview:self.view];

復現這個現象,我寫了一個下面的代碼測試,如下:

- (IBAction)btnAction:(id)sender {
    UIViewController *test01 = [[UIViewController alloc] init];
    [self.navigationController pushViewController:test01 animated:YES];
    [self.navigationController pushViewController:test01 animated:YES];
}

解決該異常最簡單的方式是把animated設置為NO,但是很不友好,把系統自帶的動畫效果給去掉了。另外一種友好的方式就是通過runtime來進行實現了,通過安全的方式,確保當有控制器正在進行入棧或出棧時,沒有其他入棧或出棧操作。具體源碼會在文章末尾提供。

SIGSEGV的崩潰記錄有57條(總共304條崩潰記錄),占18.75%。在Crash界排名第二。

殺手 NO.3:NSRangeException 異常

造成這個異常,就是越界異常了,在iOS中我們經常碰到的越界異常有兩種,一種是數組越界,一種字符串截取越界,我們通過crash日志來具體分析一下。

crash 日志3-1

-[__NSArrayM objectAtIndex:]: index 1 beyond bounds for empty array
-[__NSCFConstantString substringToIndex:]: Index 10 out of bounds; string length 0

通過日志可以很明顯的知道問題,就是越界造成的,復現該現象也比較簡單,在此就略過了。怎麼解決呢?

方案一:在對數組取數據的時候,要判斷一下數組的長度大於取的index,這個要在平時寫代碼的時候給規范起來。同樣在對字符串進行截取的時候,也需要做類似的判斷。但現實的情況是,有時我們會忘了寫這樣的邏輯判斷,就會有潛在的崩潰問題。如何做一下統一的判斷呢?即使開發人員忘了寫這樣的邏輯判斷也不會造成崩潰,從框架層面來杜絕這類的崩潰,方案二給出了答案。

方案二:利用runtime的Swizzle Method特性,可以實現從框架層面杜絕這類的崩潰問題,這樣做的好處有兩點:

  • 開發人員忘了寫判斷越界的邏輯,也不會造成app的崩潰,對開發人員來說是透明的。

  • 不需要修改現有的代碼,對現有代碼的侵入性降低到最低,不需要添加大量重復的邏輯判斷代碼。

全部源碼會在文章末尾提供,現截取部分代碼實現:

@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異常,通俗一點的描述是這樣的:對一個端已經關閉的socket調用兩次write,第二次write將會產生SIGPIPE信號,該信號默認結束進程。

那如何解決該問題呢?對SIGPIPE信號可以進行捕獲,也可將其忽略,對於iOS系統來說,只需要把下面這段代碼放在.pch文件中即可。

// 僅在 IOS 系統上支持 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內部、操作系統用終止方法拋出。通常發生在異步執行系統方法的時候。如CoreData、NSUserDefaults等,還有一些其他的系統多線程操作。

注意:這並不一定意味著是系統代碼存在bug,代碼僅僅是成了無效狀態,或者異常狀態。

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/

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved