ios 避免程式crash的有效解決方法
程式崩潰經歷
原始碼地址 https://github.com/frankzhuo/AvoidCrash 歡迎fork其實在很早之前就想寫這篇文章了,一直拖到現在。
- 程式崩潰經歷1
- 我們公司做的是股票軟體,但整合的是第三方的靜態庫(我們公司和第三方公司合作,他們提供股票的服務,我們付錢)。平時開發測試的時候好好的,結果上線幾天發現有崩潰的問題,其實責任大部分在我身上。
- 我的責任: 過分信賴文件,沒進行容錯處理,也就是沒有對資料進行相應的判斷處理。
- 下面附上程式碼,說明崩潰的原因
- 我們公司做的是股票軟體,但整合的是第三方的靜態庫(我們公司和第三方公司合作,他們提供股票的服務,我們付錢)。平時開發測試的時候好好的,結果上線幾天發現有崩潰的問題,其實責任大部分在我身上。
因第三方公司提供的資料錯亂導致有時候建立字典的時候個別value為nil才導致的崩潰
//巨集
#define CStringToOcString(cstr) [NSString stringWithCString:cstr encoding:GBK_ENCODE]
//將每組資料都儲存起來
NSMutableArray *returnArray = [NSMutableArray array];
for (int i = 0; i < recordM.count; i++) {
Withdrawqry_entrust_record *record = (Withdrawqry_entrust_record *)alloca(sizeof(Withdrawqry_entrust_record));
memset(record, 0x00, sizeof(Withdrawqry_entrust_record));
[[recordM objectAtIndex:i] getValue:record];
//崩潰的原因在建立字典的時候,有個別value為nil (CStringToOcString)
NSDictionary *param = @{
@"batch_no" : CStringToOcString(record->batch_no),// 委託批號
@"entrust_no" : CStringToOcString(record->entrust_no),// 委託編號
@"entrust_type" : @(record->entrust_type),//委託類別 6 融資委託 7 融券委託 和entrust_bs結合形成融資買入,融資賣出,融券賣出,融券買入
@"entrust_bs" : @(record->entrust_bs),// 買賣標誌
@"stock_account" : CStringToOcString(record->stock_account),//證券賬號
@"gdcode" : CStringToOcString(record->gdcode),
.....
.....
.....
};
- 解決辦法,在巨集那裡做了個判斷,若果value為nil,直接賦值為@""
#define CStringToOcString(cstr) [NSString stringWithCString:cstr encoding:GBK_ENCODE] ?
[NSString stringWithCString:cstr encoding:GBK_ENCODE] : @""
- 程式崩潰經歷2
不做過多的闡述,直接看程式碼
//伺服器返回的日期格式為20160301
//我要將格式轉換成2016-03-01
/** 委託日期 */
NSMutableString *dateStrM = 伺服器返回的資料
[dateStrM insertString:@"-" atIndex:4];
[dateStrM insertString:@"-" atIndex:7];
就是上面的程式碼導致了上線的程式崩潰,搞的我在第二天緊急再上線了一個版本。
為何會崩潰呢?原因是伺服器返回的資料錯亂了,返回了0。這樣字串的長度就為1,而卻插入下標為4的位置,程式必然會崩潰。後來在原本程式碼上加了一個判斷,如下程式碼:
if (dateStrM.length >= 8) {
[dateStrM insertString:@"-" atIndex:4];
[dateStrM insertString:@"-" atIndex:7];
}
醒悟
- 1、不要過分相信伺服器返回的資料會永遠的正確。
- 2、在對資料處理上,要進行容錯處理,進行相應判斷之後再處理資料,這是一個良好的程式設計習慣。
思考:如何防止存在潛在崩潰方法的崩潰
- 眾所周知,Foundation框架裡有非常多常用的方法有導致崩潰的潛在危險。對於一個已經將近竣工的專案,若起初沒做容錯處理又該怎麼辦?你總不會一行行程式碼去排查有沒有做容錯處理吧!-------- 別逗逼了,老闆催你明天就要上線了!
- 那有沒有一種一勞永逸的方法?無需動原本的程式碼就可以解決潛在崩潰的問題呢?
解決方案
攔截存在潛在崩潰危險的方法,在攔截的方法裡進行相應的處理,就可以防止方法的崩潰
步驟:
- 1、通過category給類新增方法用來替換掉原本存在潛在崩潰的方法。
- 2、利用runtime方法交換技術,將系統方法替換成我們給類新增的新方法。
具體實現
建立一個工具類AvoidCrash,來處理方法的交換,獲取會導致崩潰程式碼的具體位置,在控制檯輸出錯誤的資訊......
AvoidCrash.h
//
// AvoidCrash.h
// AvoidCrash
//
// Created by mac on 16/9/21.
// Copyright © 2016年 chenfanfang. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
//通知的名稱,若要獲取詳細的崩潰資訊,請監聽此通知
#define AvoidCrashNotification @"AvoidCrashNotification"
#define AvoidCrashDefaultReturnNil @"This framework default is to return nil."
#define AvoidCrashDefaultIgnore @"This framework default is to ignore this operation to avoid crash."
@interface AvoidCrash : NSObject
/**
* become effective . You can call becomeEffective method in AppDelegate didFinishLaunchingWithOptions
*
* 開始生效.你可以在AppDelegate的didFinishLaunchingWithOptions方法中呼叫becomeEffective方法
*/
+ (void)becomeEffective;
+ (void)exchangeClassMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel;
+ (void)exchangeInstanceMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel;
+ (NSString *)getMainCallStackSymbolMessageWithCallStackSymbolStr:(NSString *)callStackSymbolStr;
+ (void)noteErrorWithException:(NSException *)exception defaultToDo:(NSString *)defaultToDo;
@end
AvoidCrash.m
//
// AvoidCrash.m
// AvoidCrash
//
// Created by mac on 16/9/21.
// Copyright © 2016年 chenfanfang. All rights reserved.
//
#import "AvoidCrash.h"
//category
#import "NSArray+AvoidCrash.h"
#import "NSMutableArray+AvoidCrash.h"
#import "NSDictionary+AvoidCrash.h"
#import "NSMutableDictionary+AvoidCrash.h"
#import "NSString+AvoidCrash.h"
#import "NSMutableString+AvoidCrash.h"
#define AvoidCrashSeparator @"================================================================"
#define AvoidCrashSeparatorWithFlag @"========================AvoidCrash Log=========================="
#define key_errorName @"errorName"
#define key_errorReason @"errorReason"
#define key_errorPlace @"errorPlace"
#define key_defaultToDo @"defaultToDo"
#define key_callStackSymbols @"callStackSymbols"
#define key_exception @"exception"
@implementation AvoidCrash
/**
* 開始生效(進行方法的交換)
*/
+ (void)becomeEffective {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[NSArray avoidCrashExchangeMethod];
[NSMutableArray avoidCrashExchangeMethod];
[NSDictionary avoidCrashExchangeMethod];
[NSMutableDictionary avoidCrashExchangeMethod];
[NSString avoidCrashExchangeMethod];
[NSMutableString avoidCrashExchangeMethod];
});
}
/**
* 類方法的交換
*
* @param anClass 哪個類
* @param method1Sel 方法1
* @param method2Sel 方法2
*/
+ (void)exchangeClassMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel {
Method method1 = class_getClassMethod(anClass, method1Sel);
Method method2 = class_getClassMethod(anClass, method2Sel);
method_exchangeImplementations(method1, method2);
}
/**
* 物件方法的交換
*
* @param anClass 哪個類
* @param method1Sel 方法1
* @param method2Sel 方法2
*/
+ (void)exchangeInstanceMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel {
Method method1 = class_getInstanceMethod(anClass, method1Sel);
Method method2 = class_getInstanceMethod(anClass, method2Sel);
method_exchangeImplementations(method1, method2);
}
/**
* 獲取堆疊主要崩潰精簡化的資訊<根據正則表示式匹配出來>
*
* @param callStackSymbolStr 堆疊主要崩潰資訊
*
* @return 堆疊主要崩潰精簡化的資訊
*/
+ (NSString *)getMainCallStackSymbolMessageWithCallStackSymbolStr:(NSString *)callStackSymbolStr {
//不熟悉正則表示式的朋友,可以看我另外一篇文章,連結在下面
//http://www.jianshu.com/p/b25b05ef170d
//mainCallStackSymbolMsg的格式為 +[類名 方法名] 或者 -[類名 方法名]
__block NSString *mainCallStackSymbolMsg = nil;
//匹配出來的格式為 +[類名 方法名] 或者 -[類名 方法名]
NSString *regularExpStr = @"[-\\+]\\[.+\\]";
NSRegularExpression *regularExp = [[NSRegularExpression alloc] initWithPattern:regularExpStr options:NSRegularExpressionCaseInsensitive error:nil];
[regularExp enumerateMatchesInString:callStackSymbolStr options:NSMatchingReportProgress range:NSMakeRange(0, callStackSymbolStr.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
if (result) {
mainCallStackSymbolMsg = [callStackSymbolStr substringWithRange:result.range];
*stop = YES;
}
}];
return mainCallStackSymbolMsg;
}
/**
* 提示崩潰的資訊(控制檯輸出、通知)
*
* @param exception 捕獲到的異常
* @param defaultToDo 這個框架裡預設的做法
*/
+ (void)noteErrorWithException:(NSException *)exception defaultToDo:(NSString *)defaultToDo {
//堆疊資料
NSArray *callStackSymbolsArr = [NSThread callStackSymbols];
//獲取在哪個類的哪個方法中例項化的陣列 字串格式 -[類名 方法名] 或者 +[類名 方法名]
NSString *mainCallStackSymbolMsg = [AvoidCrash getMainCallStackSymbolMessageWithCallStackSymbolStr:callStackSymbolsArr[2]];
if (mainCallStackSymbolMsg == nil) {
mainCallStackSymbolMsg = @"崩潰方法定位失敗,請您檢視函式呼叫棧來排查錯誤原因";
}
NSString *errorName = exception.name;
NSString *errorReason = exception.reason;
//errorReason 可能為 -[__NSCFConstantString avoidCrashCharacterAtIndex:]: Range or index out of bounds
//將avoidCrash去掉
errorReason = [errorReason stringByReplacingOccurrencesOfString:@"avoidCrash" withString:@""];
NSString *errorPlace = [NSString stringWithFormat:@"Error Place:%@",mainCallStackSymbolMsg];
NSString *logErrorMessage = [NSString stringWithFormat:@"\n\n%@\n\n%@\n%@\n%@\n%@\n\n%@\n\n",AvoidCrashSeparatorWithFlag, errorName, errorReason, errorPlace, defaultToDo, AvoidCrashSeparator];
NSLog(@"%@", logErrorMessage);
NSDictionary *errorInfoDic = @{
key_errorName : errorName,
key_errorReason : errorReason,
key_errorPlace : errorPlace,
key_defaultToDo : defaultToDo,
key_exception : exception,
key_callStackSymbols : callStackSymbolsArr
};
//將錯誤資訊放在字典裡,用通知的形式傳送出去
[[NSNotificationCenter defaultCenter] postNotificationName:AvoidCrashNotification object:nil userInfo:errorInfoDic];
}
@end
建立一個NSDictionary的分類,來防止建立一個字典而導致的崩潰。NSDictionary+AvoidCrash.h
//
// NSDictionary+AvoidCrash.h
// AvoidCrash
//
// Created by mac on 16/9/21.
// Copyright © 2016年 chenfanfang. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSDictionary (AvoidCrash)
+ (void)avoidCrashExchangeMethod;
@end
NSDictionary+AvoidCrash.m
在這裡先補充一個知識點: 我們平常用的快速建立字典的方式@{key : value}; 其實呼叫的方法是dictionaryWithObjects:forKeys:count:
而該方法可能導致崩潰的原因為: key陣列中的key或者objects中的value為空
//
// NSDictionary+AvoidCrash.m
// AvoidCrash
//
// Created by mac on 16/9/21.
// Copyright © 2016年 chenfanfang. All rights reserved.
//
#import "NSDictionary+AvoidCrash.h"
#import "AvoidCrash.h"
@implementation NSDictionary (AvoidCrash)
+ (void)avoidCrashExchangeMethod {
[AvoidCrash exchangeClassMethod:self method1Sel:@selector(dictionaryWithObjects:forKeys:count:) method2Sel:@selector(avoidCrashDictionaryWithObjects:forKeys:count:)];
}
+ (instancetype)avoidCrashDictionaryWithObjects:(const id _Nonnull __unsafe_unretained *)objects forKeys:(const id<NSCopying> _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt {
id instance = nil;
@try {
instance = [self avoidCrashDictionaryWithObjects:objects forKeys:keys count:cnt];
}
@catch (NSException *exception) {
NSString *defaultToDo = @"This framework default is to remove nil key-values and instance a dictionary.";
[AvoidCrash noteErrorWithException:exception defaultToDo:defaultToDo];
//處理錯誤的資料,然後重新初始化一個字典
NSUInteger index = 0;
id _Nonnull __unsafe_unretained newObjects[cnt];
id _Nonnull __unsafe_unretained newkeys[cnt];
for (int i = 0; i < cnt; i++) {
if (objects[i] && keys[i]) {
newObjects[index] = objects[i];
newkeys[index] = keys[i];
index++;
}
}
instance = [self avoidCrashDictionaryWithObjects:newObjects forKeys:newkeys count:index];
}
@finally {
return instance;
}
}
@end
來看下防止崩潰的效果
- 正常情況下,若沒有我們上面的處理,如下程式碼就會導致崩潰
NSString *nilStr = nil; NSDictionary *dict = @{ @"key" : nilStr };
崩潰截圖如下:
崩潰截圖.png
- 若通過如上的處理,就可以避免崩潰了
[AvoidCrash becomeEffective];
控制檯的輸出截圖如下
防止崩潰控制檯輸出的資訊.png
- 若想要獲取到崩潰的詳細資訊(我們可以監聽通知,通知名為:AvoidCrashNotification):可以將這些資訊傳到我們的伺服器,或者在整合第三方收集Crash資訊的SDK中自定義資訊,這樣我們就可以防止程式的崩潰,並且又得知哪些程式碼導致了崩潰。
//監聽通知:AvoidCrashNotification, 獲取AvoidCrash捕獲的崩潰日誌的詳細資訊
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dealwithCrashMessage:) name:AvoidCrashNotification object:nil];
- (void)dealwithCrashMessage:(NSNotification *)note {
//注意:所有的資訊都在userInfo中
//你可以在這裡收集相應的崩潰資訊進行相應的處理(比如傳到自己伺服器)
NSLog(@"\n\n在AppDelegate中 方法:dealwithCrashMessage列印\n\n\n\n\n%@\n\n\n\n",note.userInfo);
}
附上一張截圖檢視通知中攜帶的崩潰資訊是如何的
AvoidCrashNotification通知的監聽.png
結束語
-
程式崩潰有崩潰的好處,就是讓開發者快速認識到自己所寫的程式碼有問題,這樣才能及時修復BUG,當然這種好處只限於在開發階段。若一個上線APP出現崩潰的問題,這問題可就大了(老闆不高興,後果很嚴重)。
-
個人建議:在釋出的時候APP的時候再用上面介紹的方法來防止程式的崩潰,在開發階段最好不用。
-
上面只是舉個例子,更多防止崩潰的方法請檢視Github原始碼 AvoidCrash,這是我最近寫的一個框架,大家可以整合到自己的專案中去,在釋出APP的時候在appDelegate的didFinishLaunchingWithOptions中呼叫方法
[AvoidCrash becomeEffective];
即可,若要獲取崩潰資訊,監聽通知即可。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[AvoidCrash becomeEffective];
//監聽通知:AvoidCrashNotification, 獲取AvoidCrash捕獲的崩潰日誌的詳細資訊
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dealwithCrashMessage:) name:AvoidCrashNotification object:nil];
return YES;
}
- (void)dealwithCrashMessage:(NSNotification *)note {
//注意:所有的資訊都在userInfo中
//你可以在這裡收集相應的崩潰資訊進行相應的處理(比如傳到自己伺服器)
NSLog(@"\n\n在AppDelegate中 方法:dealwithCrashMessage列印\n\n\n\n\n%@\n\n\n\n",note.userInfo);
}
文/chenfanfang(簡書作者)