ios 避免程序crash的有效解决方法

程序崩溃经历

源码地址 https://github.com/frankzhuo/AvoidCrash  
欢迎fork

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

  • 程序崩溃经历1
    • 我们公司做的是股票软件,但集成的是第三方的静态库(我们公司和第三方公司合作,他们提供股票的服务,我们付钱)。平时开发测试的时候好好的,结果上线几天发现有崩溃的问题,其实责任大部分在我身上。
      • 我的责任: 过分信赖文档,没进行容错处理,也就是没有对数据进行相应的判断处理。
      • 下面附上代码,说明崩溃的原因

因第三方公司提供的数据错乱导致有时候创建字典的时候个别value为nil才导致的崩溃

  1.  
    //宏
  2.  
    #define CStringToOcString(cstr) [NSString stringWithCString:cstr encoding:GBK_ENCODE]
  3.  
     
  4.  
     
  5.  
    //将每组数据都保存起来
  6.  
    NSMutableArray *returnArray = [NSMutableArray array];
  7.  
    for (int i = 0; i < recordM.count; i++) {
  8.  
    Withdrawqry_entrust_record *record = (Withdrawqry_entrust_record *)alloca(sizeof(Withdrawqry_entrust_record));
  9.  
    memset(record, 0x00, sizeof(Withdrawqry_entrust_record));
  10.  
    [[recordM objectAtIndex:i] getValue:record];
  11.  
     
  12.  
     
  13.  
    //崩溃的原因在创建字典的时候,有个别value为nil (CStringToOcString)
  14.  
     
  15.  
    NSDictionary *param = @{
  16.  
    @"batch_no" : CStringToOcString(record->batch_no),// 委托批号
  17.  
    @"entrust_no" : CStringToOcString(record->entrust_no),// 委托编号
  18.  
    @"entrust_type" : @(record->entrust_type),//委托类别 6 融资委托 7 融券委托 和entrust_bs结合形成融资买入,融资卖出,融券卖出,融券买入
  19.  
    @"entrust_bs" : @(record->entrust_bs),// 买卖标志
  20.  
    @"stock_account" : CStringToOcString(record->stock_account),//证券账号
  21.  
    @"gdcode" : CStringToOcString(record->gdcode),
  22.  
    .....
  23.  
    .....
  24.  
    .....
  25.  
     
  26.  
    };
  • 解决办法,在宏那里做了个判断,若果value为nil,直接赋值为@""
  1.  
    #define CStringToOcString(cstr) [NSString stringWithCString:cstr encoding:GBK_ENCODE] ?
  2.  
    [NSString stringWithCString:cstr encoding:GBK_ENCODE] : @""

  • 程序崩溃经历2
    不做过多的阐述,直接看代码
  1.  
    //服务器返回的日期格式为20160301
  2.  
    //我要将格式转换成2016-03-01
  3.  
     
  4.  
    /** 委托日期 */
  5.  
    NSMutableString *dateStrM = 服务器返回的数据
  6.  
     
  7.  
    [dateStrM insertString:@"-" atIndex:4];
  8.  
    [dateStrM insertString:@"-" atIndex:7];

就是上面的代码导致了上线的程序崩溃,搞的我在第二天紧急再上线了一个版本。
为何会崩溃呢?原因是服务器返回的数据错乱了,返回了0。这样字符串的长度就为1,而却插入下标为4的位置,程序必然会崩溃。后来在原本代码上加了一个判断,如下代码:

  1.  
    if (dateStrM.length >= 8) {
  2.  
    [dateStrM insertString:@"-" atIndex:4];
  3.  
    [dateStrM insertString:@"-" atIndex:7];
  4.  
    }

醒悟

  • 1、不要过分相信服务器返回的数据会永远的正确。
  • 2、在对数据处理上,要进行容错处理,进行相应判断之后再处理数据,这是一个良好的编程习惯。

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

  • 众所周知,Foundation框架里有非常多常用的方法有导致崩溃的潜在危险。对于一个已经将近竣工的项目,若起初没做容错处理又该怎么办?你总不会一行行代码去排查有没有做容错处理吧!-------- 别逗逼了,老板催你明天就要上线了!
  • 那有没有一种一劳永逸的方法?无需动原本的代码就可以解决潜在崩溃的问题呢?

解决方案

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

步骤:


具体实现

创建一个工具类AvoidCrash,来处理方法的交换,获取会导致崩溃代码的具体位置,在控制台输出错误的信息......

AvoidCrash.h

  1.  
    //
  2.  
    // AvoidCrash.h
  3.  
    // AvoidCrash
  4.  
    //
  5.  
    // Created by mac on 16/9/21.
  6.  
    // Copyright © 2016年 chenfanfang. All rights reserved.
  7.  
    //
  8.  
     
  9.  
    #import <Foundation/Foundation.h>
  10.  
    #import <objc/runtime.h>
  11.  
     
  12.  
    //通知的名称,若要获取详细的崩溃信息,请监听此通知
  13.  
    #define AvoidCrashNotification @"AvoidCrashNotification"
  14.  
    #define AvoidCrashDefaultReturnNil @"This framework default is to return nil."
  15.  
    #define AvoidCrashDefaultIgnore @"This framework default is to ignore this operation to avoid crash."
  16.  
     
  17.  
    @interface AvoidCrash : NSObject
  18.  
     
  19.  
    /**
  20.  
    * become effective . You can call becomeEffective method in AppDelegate didFinishLaunchingWithOptions
  21.  
    *
  22.  
    * 开始生效.你可以在AppDelegate的didFinishLaunchingWithOptions方法中调用becomeEffective方法
  23.  
    */
  24.  
    + (void)becomeEffective;
  25.  
     
  26.  
    + (void)exchangeClassMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel;
  27.  
     
  28.  
    + (void)exchangeInstanceMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel;
  29.  
     
  30.  
    + (NSString *)getMainCallStackSymbolMessageWithCallStackSymbolStr:(NSString *)callStackSymbolStr;
  31.  
     
  32.  
    + (void)noteErrorWithException:(NSException *)exception defaultToDo:(NSString *)defaultToDo;
  33.  
     
  34.  
    @end

AvoidCrash.m

  1.  
    //
  2.  
    // AvoidCrash.m
  3.  
    // AvoidCrash
  4.  
    //
  5.  
    // Created by mac on 16/9/21.
  6.  
    // Copyright © 2016年 chenfanfang. All rights reserved.
  7.  
    //
  8.  
     
  9.  
    #import "AvoidCrash.h"
  10.  
     
  11.  
    //category
  12.  
    #import "NSArray+AvoidCrash.h"
  13.  
    #import "NSMutableArray+AvoidCrash.h"
  14.  
     
  15.  
    #import "NSDictionary+AvoidCrash.h"
  16.  
    #import "NSMutableDictionary+AvoidCrash.h"
  17.  
     
  18.  
    #import "NSString+AvoidCrash.h"
  19.  
    #import "NSMutableString+AvoidCrash.h"
  20.  
     
  21.  
    #define AvoidCrashSeparator @"================================================================"
  22.  
    #define AvoidCrashSeparatorWithFlag @"========================AvoidCrash Log=========================="
  23.  
     
  24.  
    #define key_errorName @"errorName"
  25.  
    #define key_errorReason @"errorReason"
  26.  
    #define key_errorPlace @"errorPlace"
  27.  
    #define key_defaultToDo @"defaultToDo"
  28.  
    #define key_callStackSymbols @"callStackSymbols"
  29.  
    #define key_exception @"exception"
  30.  
     
  31.  
    @implementation AvoidCrash
  32.  
     
  33.  
    /**
  34.  
    * 开始生效(进行方法的交换)
  35.  
    */
  36.  
    + (void)becomeEffective {
  37.  
     
  38.  
    static dispatch_once_t onceToken;
  39.  
    dispatch_once(&onceToken, ^{
  40.  
     
  41.  
    [NSArray avoidCrashExchangeMethod];
  42.  
    [NSMutableArray avoidCrashExchangeMethod];
  43.  
     
  44.  
    [NSDictionary avoidCrashExchangeMethod];
  45.  
    [NSMutableDictionary avoidCrashExchangeMethod];
  46.  
     
  47.  
    [NSString avoidCrashExchangeMethod];
  48.  
    [NSMutableString avoidCrashExchangeMethod];
  49.  
     
  50.  
    });
  51.  
    }
  52.  
     
  53.  
    /**
  54.  
    * 类方法的交换
  55.  
    *
  56.  
    * @param anClass 哪个类
  57.  
    * @param method1Sel 方法1
  58.  
    * @param method2Sel 方法2
  59.  
    */
  60.  
    + (void)exchangeClassMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel {
  61.  
    Method method1 = class_getClassMethod(anClass, method1Sel);
  62.  
    Method method2 = class_getClassMethod(anClass, method2Sel);
  63.  
    method_exchangeImplementations(method1, method2);
  64.  
    }
  65.  
     
  66.  
    /**
  67.  
    * 对象方法的交换
  68.  
    *
  69.  
    * @param anClass 哪个类
  70.  
    * @param method1Sel 方法1
  71.  
    * @param method2Sel 方法2
  72.  
    */
  73.  
    + (void)exchangeInstanceMethod:(Class)anClass method1Sel:(SEL)method1Sel method2Sel:(SEL)method2Sel {
  74.  
    Method method1 = class_getInstanceMethod(anClass, method1Sel);
  75.  
    Method method2 = class_getInstanceMethod(anClass, method2Sel);
  76.  
    method_exchangeImplementations(method1, method2);
  77.  
    }
  78.  
     
  79.  
    /**
  80.  
    * 获取堆栈主要崩溃精简化的信息<根据正则表达式匹配出来>
  81.  
    *
  82.  
    * @param callStackSymbolStr 堆栈主要崩溃信息
  83.  
    *
  84.  
    * @return 堆栈主要崩溃精简化的信息
  85.  
    */
  86.  
     
  87.  
    + (NSString *)getMainCallStackSymbolMessageWithCallStackSymbolStr:(NSString *)callStackSymbolStr {
  88.  
    //不熟悉正则表达式的朋友,可以看我另外一篇文章,链接在下面
  89.  
    //http://www.jianshu.com/p/b25b05ef170d
  90.  
     
  91.  
    //mainCallStackSymbolMsg的格式为 +[类名 方法名] 或者 -[类名 方法名]
  92.  
    __block NSString *mainCallStackSymbolMsg = nil;
  93.  
     
  94.  
    //匹配出来的格式为 +[类名 方法名] 或者 -[类名 方法名]
  95.  
    NSString *regularExpStr = @"[-\\+]\\[.+\\]";
  96.  
     
  97.  
    NSRegularExpression *regularExp = [[NSRegularExpression alloc] initWithPattern:regularExpStr options:NSRegularExpressionCaseInsensitive error:nil];
  98.  
     
  99.  
    [regularExp enumerateMatchesInString:callStackSymbolStr options:NSMatchingReportProgress range:NSMakeRange(0, callStackSymbolStr.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
  100.  
    if (result) {
  101.  
    mainCallStackSymbolMsg = [callStackSymbolStr substringWithRange:result.range];
  102.  
    *stop = YES;
  103.  
    }
  104.  
    }];
  105.  
     
  106.  
     
  107.  
     
  108.  
    return mainCallStackSymbolMsg;
  109.  
    }
  110.  
     
  111.  
    /**
  112.  
    * 提示崩溃的信息(控制台输出、通知)
  113.  
    *
  114.  
    * @param exception 捕获到的异常
  115.  
    * @param defaultToDo 这个框架里默认的做法
  116.  
    */
  117.  
    + (void)noteErrorWithException:(NSException *)exception defaultToDo:(NSString *)defaultToDo {
  118.  
     
  119.  
    //堆栈数据
  120.  
    NSArray *callStackSymbolsArr = [NSThread callStackSymbols];
  121.  
     
  122.  
    //获取在哪个类的哪个方法中实例化的数组 字符串格式 -[类名 方法名] 或者 +[类名 方法名]
  123.  
    NSString *mainCallStackSymbolMsg = [AvoidCrash getMainCallStackSymbolMessageWithCallStackSymbolStr:callStackSymbolsArr[2]];
  124.  
     
  125.  
    if (mainCallStackSymbolMsg == nil) {
  126.  
     
  127.  
    mainCallStackSymbolMsg = @"崩溃方法定位失败,请您查看函数调用栈来排查错误原因";
  128.  
     
  129.  
    }
  130.  
     
  131.  
    NSString *errorName = exception.name;
  132.  
    NSString *errorReason = exception.reason;
  133.  
    //errorReason 可能为 -[__NSCFConstantString avoidCrashCharacterAtIndex:]: Range or index out of bounds
  134.  
    //将avoidCrash去掉
  135.  
    errorReason = [errorReason stringByReplacingOccurrencesOfString:@"avoidCrash" withString:@""];
  136.  
     
  137.  
    NSString *errorPlace = [NSString stringWithFormat:@"Error Place:%@",mainCallStackSymbolMsg];
  138.  
     
  139.  
    NSString *logErrorMessage = [NSString stringWithFormat:@"\n\n%@\n\n%@\n%@\n%@\n%@\n\n%@\n\n",AvoidCrashSeparatorWithFlag, errorName, errorReason, errorPlace, defaultToDo, AvoidCrashSeparator];
  140.  
    NSLog(@"%@", logErrorMessage);
  141.  
     
  142.  
    NSDictionary *errorInfoDic = @{
  143.  
    key_errorName : errorName,
  144.  
    key_errorReason : errorReason,
  145.  
    key_errorPlace : errorPlace,
  146.  
    key_defaultToDo : defaultToDo,
  147.  
    key_exception : exception,
  148.  
    key_callStackSymbols : callStackSymbolsArr
  149.  
    };
  150.  
     
  151.  
    //将错误信息放在字典里,用通知的形式发送出去
  152.  
    [[NSNotificationCenter defaultCenter] postNotificationName:AvoidCrashNotification object:nil userInfo:errorInfoDic];
  153.  
    }
  154.  
     
  155.  
    @end

创建一个NSDictionary的分类,来防止创建一个字典而导致的崩溃。
NSDictionary+AvoidCrash.h

  1.  
    //
  2.  
    // NSDictionary+AvoidCrash.h
  3.  
    // AvoidCrash
  4.  
    //
  5.  
    // Created by mac on 16/9/21.
  6.  
    // Copyright © 2016年 chenfanfang. All rights reserved.
  7.  
    //
  8.  
     
  9.  
    #import <Foundation/Foundation.h>
  10.  
     
  11.  
    @interface NSDictionary (AvoidCrash)
  12.  
     
  13.  
    + (void)avoidCrashExchangeMethod;
  14.  
     
  15.  
    @end

NSDictionary+AvoidCrash.m
在这里先补充一个知识点: 我们平常用的快速创建字典的方式@{key : value}; 其实调用的方法是dictionaryWithObjects:forKeys:count: 而该方法可能导致崩溃的原因为: key数组中的key或者objects中的value为空

  1.  
    //
  2.  
    // NSDictionary+AvoidCrash.m
  3.  
    // AvoidCrash
  4.  
    //
  5.  
    // Created by mac on 16/9/21.
  6.  
    // Copyright © 2016年 chenfanfang. All rights reserved.
  7.  
    //
  8.  
     
  9.  
    #import "NSDictionary+AvoidCrash.h"
  10.  
     
  11.  
    #import "AvoidCrash.h"
  12.  
     
  13.  
    @implementation NSDictionary (AvoidCrash)
  14.  
     
  15.  
    + (void)avoidCrashExchangeMethod {
  16.  
     
  17.  
    [AvoidCrash exchangeClassMethod:self method1Sel:@selector(dictionaryWithObjects:forKeys:count:) method2Sel:@selector(avoidCrashDictionaryWithObjects:forKeys:count:)];
  18.  
    }
  19.  
     
  20.  
    + (instancetype)avoidCrashDictionaryWithObjects:(const id _Nonnull __unsafe_unretained *)objects forKeys:(const id<NSCopying> _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt {
  21.  
     
  22.  
    id instance = nil;
  23.  
     
  24.  
    @try {
  25.  
    instance = [self avoidCrashDictionaryWithObjects:objects forKeys:keys count:cnt];
  26.  
    }
  27.  
    @catch (NSException *exception) {
  28.  
     
  29.  
    NSString *defaultToDo = @"This framework default is to remove nil key-values and instance a dictionary.";
  30.  
    [AvoidCrash noteErrorWithException:exception defaultToDo:defaultToDo];
  31.  
     
  32.  
    //处理错误的数据,然后重新初始化一个字典
  33.  
    NSUInteger index = 0;
  34.  
    id _Nonnull __unsafe_unretained newObjects[cnt];
  35.  
    id _Nonnull __unsafe_unretained newkeys[cnt];
  36.  
     
  37.  
    for (int i = 0; i < cnt; i++) {
  38.  
    if (objects[i] && keys[i]) {
  39.  
    newObjects[index] = objects[i];
  40.  
    newkeys[index] = keys[i];
  41.  
    index++;
  42.  
    }
  43.  
    }
  44.  
    instance = [self avoidCrashDictionaryWithObjects:newObjects forKeys:newkeys count:index];
  45.  
    }
  46.  
    @finally {
  47.  
    return instance;
  48.  
    }
  49.  
    }
  50.  
     
  51.  
    @end

来看下防止崩溃的效果

  • 正常情况下,若没有我们上面的处理,如下代码就会导致崩溃
    1.  
      NSString *nilStr = nil;
    2.  
      NSDictionary *dict = @{
    3.  
      @"key" : nilStr
    4.  
      };

崩溃截图如下:


崩溃截图.png

  • 若通过如上的处理,就可以避免崩溃了
    [AvoidCrash becomeEffective];

控制台的输出截图如下


防止崩溃控制台输出的信息.png
  • 若想要获取到崩溃的详细信息(我们可以监听通知,通知名为:AvoidCrashNotification):可以将这些信息传到我们的服务器,或者在集成第三方收集Crash信息的SDK中自定义信息,这样我们就可以防止程序的崩溃,并且又得知哪些代码导致了崩溃。
  1.  
    //监听通知:AvoidCrashNotification, 获取AvoidCrash捕获的崩溃日志的详细信息
  2.  
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dealwithCrashMessage:) name:AvoidCrashNotification object:nil];
  3.  
     
  4.  
     
  5.  
     
  6.  
     
  7.  
    - (void)dealwithCrashMessage:(NSNotification *)note {
  8.  
     
  9.  
    //注意:所有的信息都在userInfo中
  10.  
    //你可以在这里收集相应的崩溃信息进行相应的处理(比如传到自己服务器)
  11.  
    NSLog(@"\n\n在AppDelegate中 方法:dealwithCrashMessage打印\n\n\n\n\n%@\n\n\n\n",note.userInfo);
  12.  
    }

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


AvoidCrashNotification通知的监听.png

结束语

  • 程序崩溃有崩溃的好处,就是让开发者快速认识到自己所写的代码有问题,这样才能及时修复BUG,当然这种好处只限于在开发阶段。若一个上线APP出现崩溃的问题,这问题可就大了(老板不高兴,后果很严重)。

  • 个人建议:在发布的时候APP的时候再用上面介绍的方法来防止程序的崩溃,在开发阶段最好不用。

  • 上面只是举个例子,更多防止崩溃的方法请查看Github源码 AvoidCrash,这是我最近写的一个框架,大家可以集成到自己的项目中去,在发布APP的时候在appDelegate的didFinishLaunchingWithOptions中调用方法[AvoidCrash becomeEffective];即可,若要获取崩溃信息,监听通知即可。

  1.  
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  2.  
    [AvoidCrash becomeEffective];
  3.  
     
  4.  
    //监听通知:AvoidCrashNotification, 获取AvoidCrash捕获的崩溃日志的详细信息
  5.  
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dealwithCrashMessage:) name:AvoidCrashNotification object:nil];
  6.  
    return YES;
  7.  
    }
  8.  
     
  9.  
    - (void)dealwithCrashMessage:(NSNotification *)note {
  10.  
     
  11.  
    //注意:所有的信息都在userInfo中
  12.  
    //你可以在这里收集相应的崩溃信息进行相应的处理(比如传到自己服务器)
  13.  
    NSLog(@"\n\n在AppDelegate中 方法:dealwithCrashMessage打印\n\n\n\n\n%@\n\n\n\n",note.userInfo);
  14.  
    }

 

文/chenfanfang(简书作者)
posted @ 2020-09-01 21:17  sundayswift  阅读(1298)  评论(0)    收藏  举报