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 来源:简书 简书著做权归做者全部,任何形式的转载都请联系做者得到受权并注明出处。
相关文章
相关标签/搜索