iOS容错:runtime实用篇--和常见崩溃say good-

2018-07-30  本文已影响143人  路漫漫其修远兮Wzt

转载自:iOS runtime实用篇--和常见崩溃say good-bye!

程序崩溃经历

其实在很早之前就想写这篇文章了,一直拖到现在。

因第三方公司提供的数据错乱导致有时候创建字典的时候个别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),
      .....
      .....
      .....

                              };
#define CStringToOcString(cstr) [NSString stringWithCString:cstr encoding:GBK_ENCODE] ? [NSString stringWithCString:cstr encoding:GBK_ENCODE] : @""

    //服务器返回的日期格式为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];
   }

醒悟


思考:如何防止存在潜在崩溃方法的崩溃


解决方案

拦截存在潜在崩溃危险的方法,在拦截的方法里进行相应的处理,就可以防止方法的崩溃

步骤:


具体实现

创建一个工具类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</objc></foundation>

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</foundation>

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</nscopying>

来看下防止崩溃的效果

    NSString *nilStr = nil;  NSDictionary *dict = @{                         @"key" : nilStr
                             };

崩溃截图如下:

1594675-f6dbad6c12d275b2.png
    [AvoidCrash becomeEffective];

控制台的输出截图如下

1594675-2622b50e13cbd022.png
 //监听通知: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);
}

附上一张截图查看通知中携带的崩溃信息是如何的

1594675-284b0813751e725c.png

结束语

- (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更新

2016-10-15

源码

https://github.com/chenfanfang/AvoidCrash

上一篇下一篇

猜你喜欢

热点阅读