iOS runtime实用篇解决常见Crash
程序崩溃经历
其实在很早之前就想写这篇文章了,一直拖到现在。
程序崩溃经历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、利用异常的捕获来防止程序的崩溃,并且进行相应的处理。
具体实现
创建一个工具类AvoidCrash,来处理方法的交换,获取会导致崩溃代码的具体位置,在控制台输出错误的信息......
AvoidCrash.h
//// AvoidCrash.h// AvoidCrash//// Created by mac on 16/9/21.// Copyright ? 2016年 chenfanfang. All rights reserved.//#import #import //通知的名称,若要获取详细的崩溃信息,请监听此通知#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 @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 _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 };
崩溃截图如下:
若通过如上的处理,就可以避免崩溃了
[AvoidCrash becomeEffective];
控制台的输出截图如下
若想要获取到崩溃的详细信息(我们可以监听通知,通知名为: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); }
附上一张截图查看通知中携带的崩溃信息是如何的
结束语
程序崩溃有崩溃的好处,就是让开发者快速认识到自己所写的代码有问题,这样才能及时修复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防止崩溃的处理
源码
iOS runtime实用篇解决常见Crash的更多相关文章
- iOS runtime实用篇--和常见崩溃say good-bye!
程序崩溃经历 其实在很早之前就想写这篇文章了,一直拖到现在. 程序崩溃经历1 我们公司做的是股票软件,但集成的是第三方的静态库(我们公司和第三方公司合作,他们提供股票的服务,我们付钱).平时开发测试的 ...
- iOS runtime实用篇--和常见崩溃say good-bye
源码 https://github.com/chenfanfang/AvoidCrash 程序崩溃经历 其实在很早之前就想写这篇文章了,一直拖到现在. 程序崩溃经历1 我们公司做的是股票软件,但集成的 ...
- iOS 万能跳转界面方法 (runtime实用篇一)
http://www.cocoachina.com/ios/20150824/13104.html 作者:汉斯哈哈哈 授权本站转载. 在开发项目中,会有这样变态的需求: 推送:根据服务端推送过来的数据 ...
- iOS开发——实用篇Swift篇&QQ登入界面实现
QQ登入界面实现 我们知道在App Store中几乎所有软件都设计到账户的登入,而我们最常见的就是QQ,微信,在没有踏入程序员这条不归路之前,看到一个个的界面都感觉好高大上的样子. 在学习的过程中,自 ...
- iOS开发——实用篇&提高iOS开发效率的方法和工具
提高iOS开发效率的方法和工具 介绍 这篇文章主要是介绍一下我在iOS开发中使用到的一些可以提升开发效率的方法和工具. IDE 首先要说的肯定是IDE了,说到IDE,Xcode不能跑,当然你也可能同时 ...
- iOS开发——实用篇Swift篇&项目开发常用实用技术
项目开发常用实用技术 实现拨打电话 要实现打电话功能,最简单最直接的方式便是:直接跳到拨号界面 (注意:这个需要真机调试,模拟器无效果) UIApplication.sharedApplica ...
- iOS开发——实用篇&KVO与KVC详解
KVO与KVC详解 由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby.Python的动态特性,例如动态类型.动态加载.动态绑定等.今天我们着重介绍ObjC中的键值编码(KVC ...
- 跳转界面方法 (runtime实用篇一)
在开发项目中,会有这样变态的需求: 推送:根据服务端推送过来的数据规则,跳转到对应的控制器 feeds列表:不同类似的cell,可能跳转不同的控制器(嘘!产品经理是这样要求:我也不确定会跳转哪个界面哦 ...
- iOS开发——实用篇Swift篇&状态栏操作
状态栏操作 在Swift开发过程中,针对状态栏操作的过程有很多. 1.在ViewController中操作当前ViewController的状态栏 /** 隐藏状态栏 */ override func ...
随机推荐
- django 1.7之后python manage.py syncdb没有了
在命令行输入python manage.py createsuperuser按照提示输入即可记得先初始化表. django>1.7 python manage.py makemigrations ...
- spring+mybatis 手动开启和提交事务
spring配置文件 事务控制管理器transactionManager <!-- (事务管理)transaction manager, use JtaTransactionManager fo ...
- lkx开发日志2-第一次团队讨论
遇到的问题 冰球与击球手碰撞的形式有两种.第一种:击球手的速度不指向冰球圆心,这样碰撞后冰球会旋转.第二种:击球手的速度指向冰球圆心,直接科运用动量定理计算两者速度的变化.考虑到时间限制,团队假设冰球 ...
- centos 安装pptp
1. 安装依赖 ppp yum -y install ppp 2. 编译安装pptpd wget http://jaist.dl.sourceforge.net/project/poptop/pptp ...
- linux------------centos防火墙
CentOS7默认的防火墙不是iptables,而是firewalle. 你可以用rpm -qa | grep iptables来查看,一般会出现两个一个是iptables 另一个是iptables. ...
- eclipse自动补全快捷键失效,sysout用不了!
好久没写Java代码了,使用新版Neon的Eclipse Java EE IDE开发时,自动补全各种失败,sysout也各种用不了, 开始还以为是电脑卡比呢,原来是版本的快捷键不同了,修改方法如下! ...
- 基于Centos7+Nginx+Tomcat8的负载均衡服务器的搭建
由于工作的需求,在使用中,需要搭建负载均衡,研究了Apache+Tomat负载均衡的方案,并且通过检索相关的文章,进行了比较发现,Apache负载负载均衡在使用的效率上,远远不如Nginx的效率高,因 ...
- TCPReplay使用---张子芳
TCPReplay主要功能是将PCAP包重新发送,用于性能或者功能测试.但是在测试环境与原转包系统结构一般是不同的.比如被测试机的二层MAC地址与抓包机器的MAC不同,所以被测试机在二层处理时发现目的 ...
- EF Core 1.0 和 SQLServer 2008 分页的问题
EF Core 1.0 在sqlserver2008分页的时候需要指定用数字分页. EF Core1.0 生成的分页语句中使用了 Featch Next.这个语句只有在SqlServer2012的时候 ...
- C# 不重复的随机数
public int RabdomNumber() { num = new Random(Guid.NewGuid().GetHashCode()).Next(0, 40); return num; ...