iOS Crash处理方法(一):利用MethodSwizzle避免Crash

前段时间APP项目上线之后,会有一些闪退的情况,而这些Crash大多数是属于内存越界、键值为空以及字符串的问题,因为现在用的Crash收集是友盟SDK,会出现很多无法定位的Crash(Application received signal SIGABRT)。
所以我就写了这个阻止Crash的方法,基本的原理就是用MethodSwizzle,通过替换原有类的方法,换成自己写的方法,然后在自己的方法里面处理异常情况。


这是项目的github地址:https://github.com/WalkingToTheDistant/CrashManager,这篇文章只是概述一些关键点。


使用方法

首先,从Github上下载代码压缩文件并解压,会得到CrashManager-master文件夹,把整个文件夹拖动添加到工程中。
要启用这个功能,需要调用 CrashPublic.h 里面的 openCrashHandleFunction 方法

CrashPublic.h
/** 开启防止Crash的功能,可以在application:didFinishLaunchingWithOptions 方法里面调用 */
+ (void) openCrashHandleFunction;

openCrashHandleFunction 方法,会调用各个分类Category的初始化方法,这里也许会有人奇怪,为什么各个Category还要显式写一个方法进行初始化,而不是在Category的load或者initialize 里面执行?
在这里说明一下这几个方法的区别

  • initialize:首先 initialize 方法是只有在类方法第一次被使用的才会被调用,而且子类的initialize会触发父类的initialize(不管这个方法是子类还是父类的,父类的initialize不会触发子类)。调用顺序:父类 -> 子类,而在类别Category触发initialize也是这个顺序,(注意:类别的initialize会“覆盖”原类的initialize,而load不会,这就是为什么不在initialize实现方法替换的原因,避免父类的initialize被category覆盖而不被调用)
  • load:load是只要类所在文件被引用就会被调用,而调用的顺序是:父类 -> 子类 -> 父类的类别 -> 子类的类别,类别中重写load不会覆盖原类的方法。

关于load和initialize 的区别,这篇文章讲的蛮好的:传送入门

那么重点来了,为什么不在这几个方法里面执行交换呢?
首先是 + (void) load,load的执行顺序中,类别是最后才被执行,而很多系统库的API都是用类别实现的,比如NSMutableString (NSMutableStringExtensionMethods)。而类别的load顺序并不是固定的(跟Compile Sources里面的文件顺序有关,传送入口),那么就会出现一种情况,在load执行MethodSwizzle时,也许要替换的方法还没有加载到方法列表里面(找不到该方法指针),而该类的方法是在自己的load调用之前才会加载到原类的方法列表里面去。
再然后是 + (void) initialize,因为我们在类别中重新写了这个方法,所以这个会覆盖原类的方法,那么就有一个问题了,如果在原类或者其子类中触发了initialize,那么该类别的initialize就会被调用。例如在子类的load中调用了其类方法,那么触发父类和子类的initialize,而这个时候,有可能类别的方法也就没加载到类方法列表了。
所以就在CrashPublic显示写一个初始化方法openCrashHandleFunction,这样也可以在不需要使用CrashHandle的时候,直接屏蔽这句方法可以了。

另外还有一个方法可以考虑,那就是消息监听:

  • NSNotificationCenter:监听UIApplicationDidFinishLaunchingNotification,是在application:didFinishLaunchingWithOptions执行结束之后会触发监听
#import <UIKit/UIKit.h>
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(handleObserver:) name:UIApplicationDidFinishLaunchingNotification object:nil];

类簇

先简单看下关于NSArray类的处理

void MethodSwizzle(Class cls , SEL oriSelector, SEL dstSelector)
{
    if(cls == nil){ return; }
    
    Method oriMethod = class_getInstanceMethod(cls, oriSelector);
    Method dstMethod = class_getInstanceMethod(cls, dstSelector);
    BOOL isAdd = class_addMethod(cls, oriSelector, method_getImplementation(dstMethod), method_getTypeEncoding(dstMethod));
    if(isAdd == YES
       && oriMethod != nil){
        class_replaceMethod(cls, dstSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else {
        method_exchangeImplementations(oriMethod, dstMethod);
    }
}
+ (void)initCrashCategory
{    
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        //__NSSingleObjectArrayI, __NSArray0
        MethodSwizzle(objc_getClass("__NSArrayI"), @selector(objectAtIndex:), @selector(arrayI_crashSafe_objectAtIndex:));
        MethodSwizzle(objc_getClass("__NSArray0"), @selector(objectAtIndex:), @selector(array0_crashSafe_objectAtIndex:));
        MethodSwizzle(objc_getClass("__NSSingleObjectArrayI"), @selector(objectAtIndex:), @selector(singleObjectArrayI_crashSafe_objectAtIndex:));
    });
}

为什么不是直接对[NSArray class]进行处理,而是需要处理__NSArrayI、__NSArray0和__NSSingleObjectArrayI,你可以试试把上面的代码替换成下面这种,然后执行调试(可以在 crashSafe_objectAtIndex 里面增加一个断点)。

+ (void)initCrashCategory
{    
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        //__NSSingleObjectArrayI, __NSArray0
        MethodSwizzle([NSArray class], @selector(objectAtIndex:), @selector(crashSafe_objectAtIndex:));
    });
}

如果你尝试之后,那么会发现,category里面的crashSafe_objectAtIndex的方法都不会被调用。
可能你对OC的类簇还比较陌生,简单说,我们平时代码里面用的NSArray、NSDictionary、NSString类,其实都是抽象类,在代码编译执行之后,这些类对象都会根据情况编译成 __NSArrayI、__NSArray0 这样的类实体对象(调试时在lldb输出对象的class就明白了),可以简单从类名称就知道其含义:__NSSingleObjectArrayI是Array只有一个元素的数组对象,__NSArray0则是没有元素的数组对象,__NSArrayI就是我们平时用的多元素数组。
下面这是各个类的类簇列表:

抽象类 类簇
NSArray __NSArray0、__NSSingleObjectArrayI、__NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionary0、__NSSingleEntryDictionaryI、__NSDictionaryI
NSMutableDictionary __NSDictionaryM
NSString __NSCFString、NSTaggedPointerString、 __NSCFConstantString

异常处理

当数组越界时,这是我在代码里面的处理

NSArray+Crash.h
- (id) arrayI_crashSafe_objectAtIndex:(NSUInteger)index
{
    if(index >= self.count){ // 超出索引
        return indexCrashHandle([self class]); // 然后处理消息转发
    }
    
    return [self arrayI_crashSafe_objectAtIndex:index]; // 这个方法已经被替换成原先的objectAtIndex
}

CrashPublic.h
id indexCrashHandle(Class cls)
{
    return [CrashManager new];
}

CrashManager是的NSObject的自定义子类,也许有人会问了,既然数组索引超出了范围,那为什么不返回一个nil呢,为何返回一个CrashManager对象,CrashManager又是干嘛用的?
首先先回答为什么返回一个类对象,那是因为如果返回nil,很多代码后续也没有对象判断是否为空的操作,那么也许又有疑问了,OC本身用空指针调用方法也不会出错呀!这个是没错,可是如果把空指针当做参数调用呢?就像下面这句代码

NSString *strObj = dicTemp1[@"key"]; // 这个key不存在,返回nil
NSString *strTemp = @"tempStr";
strTemp = [string stringByAppendingString:strObj]; // 这句会闪退
/* Crash信息: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSCFConstantString stringByAppendingString:]: nil argument' */

当然,如果单单返回一个CrashManager类对象,而没有在CrashManager里面做任何处理,上面的代码依然会报错,只是报错的信息变成了unrecognized selector sent to instance,这时候这就需要我们对CrashManager做 消息转发 的处理了!
CrashManager里面的消息转发比较简单,在捕获到方法找不到的异常之后,手动添加一个返回0的空方法,并告诉系统调用这个方法,这样代码就可以继续执行。

/** 处理Crash的方法 */
int handleInstanceMethodCrashMethod()
{
    return 0; // 通用返回值
}
/** 找不到方法,触发消息转发 */
+ (BOOL) resolveInstanceMethod:(SEL)sel
{
    class_addMethod(self, sel, (IMP)handleInstanceMethodCrashMethod, "v@:"); // 手动添加handleInstanceMethodCrashMethod方法作为sel的执行者
    return [super resolveInstanceMethod:sel]; // 返回YES则是告诉系统再次尝试执行这个sel
}

接下来我们用测试代码试试这个功能
测试用例1:

NSString *strObj = dicTemp1[@"key"]; // 这个key不存在,返回nil
NSString *strTemp = @"tempStr";
strTemp = [string stringByAppendingString:strObj]; 

运行会触发 resolveInstanceMethod 方法,输出sel: (SEL) sel = "length",那么如果 handleInstanceMethodCrashMethod 返回 0,那就告诉执行方法说这个strObj字符串长度为0,那么系统也不会继续执行添加strObj的操作,返回"tempStr" 的结果。

测试用例2:

NSArray *aryObj = dicTemp1[nil];
NSLog(@"%@", aryObj[1]);

触发resolveInstanceMethod ,输出sel:(SEL) sel = "objectAtIndexedSubscript:",那么handleInstanceMethodCrashMethod 返回0
,那么Log会输出:

2017-07-03 15:01:18.191 TempPro[9111:1441321] (null)

依然没有Crash,问题解决了!

结语

目前这个CrashHandle仅仅用于一个项目,还没有大量使用,所以可能里面还存在一些问题,我会继续更新Github上的代码。
另外这个Crash仅仅解决了内存越界,线上APP还存在有野指针的Crash问题,目前我还没有想到比较好的解决方案,如果想出来了,会继续更新文章。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • iOS开发中总能看到+load和+initialize的身影,网上对于这两个方法有很多解释,官方也有说明,但有些细...
    朱晓辉阅读 27,412评论 19 139
  • Objective C类方法load和initialize的区别过去两个星期里,为了完成一个工作,接触到了NSOb...
    亦晴工作室阅读 1,307评论 0 10
  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 909评论 0 6
  • 遥远的天街,俏皮的星星在眨眼; 我缄默不语,仿佛听到了往昔; 那些被略过的回忆。 我的时光里,留下很多你的脚印; ...
    童淑阅读 444评论 0 1