iOS runtime實用篇--和常見崩潰say good-bye

源碼

https://github.com/chenfanfang/AvoidCrashjavascript

 

程序崩潰經歷

其實在很早以前就想寫這篇文章了,一直拖到如今。css

  • 程序崩潰經歷1
  • 咱們公司作的是股票軟件,但集成的是第三方的靜態庫(咱們公司和第三方公司合做,他們提供股票的服務,咱們付錢)。平時開發測試的時候好好的,結果上線幾天發現有崩潰的問題,其實責任大部分在我身上。
    • 個人責任: 過度信賴文檔,沒進行容錯處理,也就是沒有對數據進行相應的判斷處理。
    • 下面附上代碼,說明崩潰的緣由

因第三方公司提供的數據錯亂致使有時候建立字典的時候個別value爲nil才致使的崩潰java

//宏 #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] : @""git

---


- 程序崩潰經歷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];
}github

---
醒悟
===
- 一、不要過度相信服務器返回的數據會永遠的正確。 - 二、在對數據處理上,要進行容錯處理,進行相應判斷以後再處理數據,這是一個良好的編程習慣。 --- 思考:如何防止存在潛在崩潰方法的崩潰 === - 衆所周知,Foundation框架裏有很是多經常使用的方法有致使崩潰的潛在危險。對於一個已經將近竣工的項目,若起初沒作容錯處理又該怎麼辦?你總不會一行行代碼去排查有沒有作容錯處理吧!-------- 別逗逼了,老闆催你明天就要上線了! - 那有沒有一種一勞永逸的方法?無需動本來的代碼就能夠解決潛在崩潰的問題呢? --- 解決方案 === 攔截存在潛在崩潰危險的方法,在攔截的方法裏進行相應的處理,就能夠防止方法的崩潰 步驟: - 一、經過category給類添加方法用來替換掉本來存在潛在崩潰的方法。 - 二、利用runtime方法交換技術,將系統方法替換成咱們給類添加的新方法。 - 三、利用異常的捕獲來防止程序的崩潰,而且進行相應的處理。 - [若是對異常NSException不瞭解,能夠點擊查看NSException的介紹。](http://www.jianshu.com/p/05aad21e319e) --- 具體實現 === 建立一個工具類AvoidCrash,來處理方法的交換,獲取會致使崩潰代碼的具體位置,在控制檯輸出錯誤的信息...... - [代碼中有正則表達式的知識點,不熟悉正則表達式的朋友們點我](http://www.jianshu.com/p/b25b05ef170d) `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數組

/**ruby

  • become effective . You can call becomeEffective method in AppDelegate didFinishLaunchingWithOptions
  • 開始生效.你能夠在AppDelegate的didFinishLaunchingWithOptions方法中調用becomeEffective方法
    */
  • (void)becomeEffective;bash

  • (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](http://upload-images.jianshu.io/upload_images/1594675-f6dbad6c12d275b2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) --- - 若經過如上的處理,就能夠避免崩潰了 

[AvoidCrash becomeEffective];

控制檯的輸出截圖以下

![防止崩潰控制檯輸出的信息.png](http://upload-images.jianshu.io/upload_images/1594675-2622b50e13cbd022.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - 若想要獲取到崩潰的詳細信息(咱們能夠監聽通知,通知名爲: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](http://upload-images.jianshu.io/upload_images/1594675-284b0813751e725c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) --- 結束語 === - 程序崩潰有崩潰的好處,就是讓開發者快速認識到本身所寫的代碼有問題,這樣才能及時修復BUG,固然這種好處只限於在開發階段。若一個上線APP出現崩潰的問題,這問題可就大了(老闆不高興,後果很嚴重)。 - 我的建議:在發佈的時候APP的時候再用上面介紹的方法來防止程序的崩潰,在開發階段最好不用。 - 上面只是舉個例子,更多防止崩潰的方法請查看Github源碼 [AvoidCrash](https://github.com/chenfanfang/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);
    }

- 同時但願你們可以提出更多容易致使崩潰的方法,我好添加到AvoidCrash框架中,固然也歡迎你們和我一塊兒維護這個框架。
- 最後,但願你們給上大家珍貴的一票(帥哥、美女,給個star哈)。


---

---

---

---

---

[AvoidCrash](https://github.com/chenfanfang/AvoidCrash)更新 === ####2016-10-15 - 修復上一個版本部分方法不能攔截崩潰的BUG,具體修復哪些能夠查看issues和簡書上的留言。 - 優化崩潰代碼的定位,定位崩潰代碼更加準確。 - 增長對KVC賦值防止崩潰的處理。 - 增長對NSAttributedString防止崩潰的處理 - 增長對NSMutableAttributedString防止崩潰的處理 ####2016-11-29 - 修復在鍵盤彈出狀態下,按Home鍵進入後臺會致使崩潰的bug。 - 新增防止崩潰(NSArray、NSMutableArray) - (NSArray *)objectsAtIndexes:(NSIndexSet *)indexes #### 2016-12-1 - 處理數組的類簇問題,提升兼容性,不管是因爲array[100]方式,仍是[array objectAtIndex:100]方式 獲取數組中的某個元素操做不當而致使的crash,都能被攔截防止崩潰。 - 上一個版本只能防止array[100]致使的崩潰,不能防止[array objectAtIndex:100]致使的崩潰。 - 統一對線程進行處理,監聽通知AvoidCrashNotification後,不管是在主線程致使的crash仍是在子線程致使的crash,監聽通知的方法統一在"主線程"中。 - 上一個版本中,在哪一個線程致使的crash, 則監聽通知的方法就在哪一個線程中。 - 新增防止崩潰 (NSArray、NSMutableArray) `- (void)getObjects:(__unsafe_unretained id _Nonnull *)objects range:(NSRange)range` 
做者:chenfanfang 連接:https://www.jianshu.com/p/5d625f86bd02 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。
相關文章
相關標籤/搜索