源码

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框架里有非常多常用的方法有导致崩溃的潜在危险。对于一个已经将近竣工的项目,若起初没做容错处理又该怎么办?你总不会一行行代码去排查有没有做容错处理吧!-------- 别逗逼了,老板催你明天就要上线了!
- 那有没有一种一劳永逸的方法?无需动原本的代码就可以解决潜在崩溃的问题呢?
--- 解决方案
===
拦截存在潜在崩溃危险的方法,在拦截的方法里进行相应的处理,就可以防止方法的崩溃 步骤:
- 1、通过category给类添加方法用来替换掉原本存在潜在崩溃的方法。
- 2、利用runtime方法交换技术,将系统方法替换成我们给类添加的新方法。
- 3、利用异常的捕获来防止程序的崩溃,并且进行相应的处理。
- [如果对异常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

/**

  • 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](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
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

iOS runtime实用篇--和常见崩溃say good-bye的更多相关文章

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

    程序崩溃经历 其实在很早之前就想写这篇文章了,一直拖到现在. 程序崩溃经历1 我们公司做的是股票软件,但集成的是第三方的静态库(我们公司和第三方公司合作,他们提供股票的服务,我们付钱).平时开发测试的 ...

  2. iOS runtime实用篇解决常见Crash

    程序崩溃经历 其实在很早之前就想写这篇文章了,一直拖到现在. 程序崩溃经历1 平时开发测试的时候好好的,结果上线几天发现有崩溃的问题,其实责任大部分在我身上. 我的责任: 过分信赖文档,没进行容错处理 ...

  3. iOS 万能跳转界面方法 (runtime实用篇一)

    http://www.cocoachina.com/ios/20150824/13104.html 作者:汉斯哈哈哈 授权本站转载. 在开发项目中,会有这样变态的需求: 推送:根据服务端推送过来的数据 ...

  4. iOS开发——实用篇Swift篇&QQ登入界面实现

    QQ登入界面实现 我们知道在App Store中几乎所有软件都设计到账户的登入,而我们最常见的就是QQ,微信,在没有踏入程序员这条不归路之前,看到一个个的界面都感觉好高大上的样子. 在学习的过程中,自 ...

  5. iOS开发——实用篇&提高iOS开发效率的方法和工具

    提高iOS开发效率的方法和工具 介绍 这篇文章主要是介绍一下我在iOS开发中使用到的一些可以提升开发效率的方法和工具. IDE 首先要说的肯定是IDE了,说到IDE,Xcode不能跑,当然你也可能同时 ...

  6. iOS开发——实用篇Swift篇&项目开发常用实用技术

    项目开发常用实用技术 实现拨打电话 要实现打电话功能,最简单最直接的方式便是:直接跳到拨号界面 (注意:这个需要真机调试,模拟器无效果)     UIApplication.sharedApplica ...

  7. iOS开发——实用篇&KVO与KVC详解

    KVO与KVC详解 由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby.Python的动态特性,例如动态类型.动态加载.动态绑定等.今天我们着重介绍ObjC中的键值编码(KVC ...

  8. 跳转界面方法 (runtime实用篇一)

    在开发项目中,会有这样变态的需求: 推送:根据服务端推送过来的数据规则,跳转到对应的控制器 feeds列表:不同类似的cell,可能跳转不同的控制器(嘘!产品经理是这样要求:我也不确定会跳转哪个界面哦 ...

  9. iOS开发——实用篇Swift篇&状态栏操作

    状态栏操作 在Swift开发过程中,针对状态栏操作的过程有很多. 1.在ViewController中操作当前ViewController的状态栏 /** 隐藏状态栏 */ override func ...

随机推荐

  1. shell while 语句

    普通循环格式: while condition do command done 例子一 #!/bin/bash )) do echo $int let "int++" done 结 ...

  2. python3.7 安装pyqt5

    pip install pyqt5

  3. ros 查找包路径

    rospack find 包名

  4. Random随机类

    // 创建随机数对象 Random random = new Random(); System.out.println("随机的boolean值" + random.nextBoo ...

  5. js 转义

    1. JavaScript 特殊字符 2. 正反斜杠互相替换 'a/b/c'.replace(/\//g,'\\')      //  "a\b\c" $0.value.repla ...

  6. Cocoapods 报警告Automatically assigning platform ios with version 9.0 on target....

    Automatically assigning platform iOS with version 9.0 on target 你的工程名称 because no platform was speci ...

  7. Java 常用对象-StringBuffer类

    2017-11-02 20:57:02 StringBuffer:线程安全的可变字符序列.一个类似于 String 的字符串缓冲区,但不能修改.虽然在任意时间点上它都包含某种特定的字符序列,但通过某些 ...

  8. [Java学习] Java虚拟机(JVM)以及跨平台原理

    相信大家已经了解到Java具有跨平台的特性,可以“一次编译,到处运行”,在Windows下编写的程序,无需任何修改就可以在Linux下运行,这是C和C++很难做到的. 那么,跨平台是怎样实现的呢?这就 ...

  9. 20161208xlVBA工作表数据导入Access

    Sub InsertToDataBase() Dim DataPath As String Dim SQL As String Const DataName As String = "yun ...

  10. 使用路径arc-奥运五环

    <!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>    < ...