iOS runtime实用篇--和常见崩溃say good-bye!

say no.jpg

源码

https://github.com/chenfanfang/AvoidCrash


程序崩溃经历

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

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

解决方案

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

步骤:


具体实现

创建一个工具类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);
}
  • 同时希望大家能够提出更多容易导致崩溃的方法,我好添加到AvoidCrash框架中,当然也欢迎大家和我一起维护这个框架。
  • 最后,希望大家给上你们珍贵的一票(帅哥、美女,给个star哈)。





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










最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容