iOS应用Crash保护系统

源码地址

开发过程中,即使我们很注意的去写代码,但是还是不能百分百的保证避免程序的Crash;iOS应用Crash保护系统 的设计初衷,就是降低APP的崩溃率。利用Objective-C语言的动态特性,采用面向切面编程的设计思想,做到无痕植入。能够自动在APP运行时实时捕获导致APP崩溃的原因,然后通过特定的技术手段去解决这些问题,使APP免于崩溃,继续运行,为APP的持续运转保驾护航。


功能简介

iOS应用Crash保护系统 计划解决程序运行过程中的大部分崩溃,但也有一些比较难发生的崩溃没有找到具体原因和解决方案,该方案主要从以下几个方面进行处理:

  • unrecognized selector引起的崩溃
  • 容器类数据类型操作引起的崩溃
  • 字符串操作引起的崩溃
  • KVO引起的崩溃
  • NSTimer引起的崩溃
  • 非主线程刷新UI
  • 野指针
  • NSNotification引起的崩溃

实现原理

unrecognized selector引起的崩溃的防护

unrecognized selector的崩溃在APP中占了很大比例,具体造成原因通常是:一个对象调用一个自己没有实现的方法造成的;
例如:

NSObject *obj = [NSObject new];
[obj methodNoRealize];

具体错误原因如下:
-[obj methodNoRealize]: unrecognized selector sent to instance 0x60000087xxxxx

要解决该类问题,我们可以从OC消息转发的过程中找到答案;首先看一下方法调用和消息转发流程:
当对象obj调用方法methodNoRealize时,会执行以下步骤
1.首先,在obj对应类的缓存方法列表中找methodNoRealize,如果找到,转向相应实现并执行;
2.如果没找到,在obj的方法列表中找methodNoRealize,如果找到,转向相应实现执行;
3.如果没找到,去父类指针所指向的对象中执行1,2;
4.以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制;

消息转发机制如下图:

消息转发流程.jpg

在消息转发流程中,有三次机会可以“拯救”没有实现的方法引起的崩溃,分别再消息转发的三步流程中,我们可以通过HOOK这三步的方法实现对该类崩溃的保护;
1.+ (BOOL)resolveInstanceMethod:(SEL)selobj找不到methodNoRealize之后,最先执行该方法,此方法返回值是BOOL,没有找到就是NO,找到就返回YES,
在此方法中解决的方法:在obj的类中加入methodNoRealize,并绑定方法实现,具体操作如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel
{    
    NSMethodSignature* sign = [self methodSignatureForSelector:sel];
    if (!sign) {
        class_addMethod([self class], sel, (IMP)unrecognizedSelector, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
- (void)unrecognizedSelector
{
    // do something
}

此步操作可以解决该问题,但是会在类中添加一个方法,在开发过程中,往往不是因为自己本类方法没有实现引起这种崩溃,而是对象类型判断错误导致,这样就会给未知的类中添加一个方法,对类造成污染;故,在这里解决可行,但不是最佳的方式;
2.- (id)forwardingTargetForSelector:(SEL)aSelector:当第一步返回结果为NO时,就会走到该方法中,在这个方法中,可以将obj查找不到的方法转发到另外一个对象中去,在另外对象中进行处理;具体操作如下:

- (id)safeForwardingTargetForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [self methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        
        id obj  = [[HYUnrecognizedSelectorHandle alloc] init];
        IMP imp = class_getMethodImplementation([HYUnrecognizedSelectorHandle class], @selector(unrecognizedSelector));
        class_addMethod([obj class], aSelector, imp, "v@:");
        return obj;
    }
    
    return [self safeForwardingTargetForSelector:aSelector];
}

在此步中,可以实例化一个预先写好的类的对象HYUnrecognizedSelectorHandle,然后获取该类的unrecognizedSelector方法的实现,将该实现,绑定给该类的名称为aSelector的方法,然后将该对象返回,这样我们就可以再unrecognizedSelector统一处理该种错误;
这种方式是最多的实现方式,因为既不污染对象对应的类,又比下一步处理的时候消耗要小,但是在这种方式中存在一个问题,在经过测试发现,在调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法过程中,某些类遵循了一些协议,但是没有实现协议方法的时候,该方法也会返回为方法签名,这样就会跳过将未实现方法转嫁给另外一个类的步骤,就不能实现保护功能;也就是说:某些类遵循了一些协议,但是没有实现协议方法的时候,在此步的解决方案不能生效
3.- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation:在此步中,有三种情况需要处理,详细如下:


- (NSMethodSignature *)safeMethodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [self safeMethodSignatureForSelector:aSelector];
    if (methodSignature) return methodSignature;

    
    IMP originIMP       = class_getMethodImplementation([NSObject class], @selector(methodSignatureForSelector:));
    IMP currentClassIMP = class_getMethodImplementation([self class],     @selector(methodSignatureForSelector:));
    // 如果子类重载了该方法,则返回nil
    if (originIMP != currentClassIMP) return nil;

    
    // - (void)xxxx
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

- (void)safeForwardInvocation:(NSInvocation *)invocation
{
    NSString *reason = [NSString stringWithFormat:@"class:[%@] not found selector:(%@)",NSStringFromClass(self.class),NSStringFromSelector(invocation.selector)];

    NSException *exception = [NSException exceptionWithName:@"Unrecognized Selector"
                                                     reason:reason
                                                   userInfo:nil];
    // 收集错误信息
    hy_handleErrorWithException(exception);

}

在以上代码中,- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 处理了三种情况:
1.如果有方法签名,则正常流程;
2.如果没有方法签名,则返回一个默认的方法签名,然后在- (void)forwardInvocation:(NSInvocation *)anInvocation中处理;
3.如果子类重载了该方法,则返回nil,具体处理交给子类;

综上所述,unrecognized selector引起的崩溃,在第三步中处理为最佳实践方案,此方案虽然相对比第二步中处理会效率略低,但可以解决第二步中解决不了的问题;

容器类数据类型操作引起的崩溃

数组、字典、集合等是我们开发中经常使用的数据类型,在使用中经常会出现数组越界、字典插入空值、集合越界等错误引起的崩溃;在开发中这种崩溃可以及时提醒我们改正错误,但是如果在线上也因为这种错误引起崩溃,对用户来说,体验是很不友好的;
对于这种崩溃的保护,采用的方法是:对容易出现异常的方法进行HOOK,然后再自定义实现中,对异常进行处理,并对异常进行收集、上报;
对这三种数据类型,目前对常用的方法进行了HOOK,做了异常保护,后续可以对所有有可能发生异常的方法进行HOOK;

数组:

+ (instancetype)arrayWithObject:(id)anObject;
- (id)objectAtIndex:(NSUInteger)index;
- (id)objectAtIndexedSubscript:(NSInteger)index;
- (NSArray *)subarrayWithRange:(NSRange)range;
+ (instancetype)arrayWithObjects:(const id [])objects count:(NSUInteger)cnt;
- (void)addObject:(id)anObject;
- (id)objectAtIndex:(NSUInteger)index;
- (id)objectAtIndexedSubscript:(NSInteger)index;
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void)removeObjectAtIndex:(NSUInteger)index;
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;
- (void)removeObjectsInRange:(NSRange)range;
- (NSArray *)subarrayWithRange:(NSRange)range;

字典:

+ (instancetype)dictionaryWithObject:(ObjectType)object forKey:(KeyType <NSCopying>)key;
+ (instancetype)dictionaryWithObjects:(const ObjectType _Nonnull [_Nullable])objects forKeys:(const KeyType <NSCopying> _Nonnull [_Nullable])keys count:(NSUInteger)cnt;
- (instancetype)initWithObjectsAndKeys:(id)firstObject, ... ;
- (instancetype)initWithObjects:(NSArray<ObjectType> *)objects forKeys:(NSArray<KeyType <NSCopying>> *)keys;
- (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;
- (void)setObject:(nullable ObjectType)obj forKeyedSubscript:(KeyType <NSCopying>)key ;
- (void)removeObjectForKey:(KeyType)aKey;

集合:

+ (instancetype)setWithObject:(ObjectType)object;
- (void)addObject:(ObjectType)object;
- (void)removeObject:(ObjectType)object;

字符串操作引起的崩溃

字符串是我们开发中使用场景最多的数据类型,对字符串进行操作也是很容易引起崩溃;例如:字符串截取、字符串拼接、删除指定范围内子串、判断是否包含子串等,如果开发中不注意 很容易引起程序崩溃;
对于这种崩溃的保护,我们采用和容器类数据类型相似的方法:对容易出现异常的方法进行HOOK,然后再自定义实现中,对异常进行处理,并对异常进行收集、上报;

KVO引起的崩溃

KVO:即Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象的监听者就会接受收到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。

KVO机制在iOS的很多开发场景中都会被使用到。不过如果一不小心使用不当的话,会导致Crash问题。KVO引起的Crash主要包含以下两个方面:

  • KVO的被观察者dealloc时仍然注册着KVO导致的Crash;
  • KVO重复添加观察者或重复移除;

针对以上问题,采用以下解决方案:
由于绝大部分的问题都是因为KVO监听对象属性过多造成的混乱,导致在开发过程中不能很好的手动管理,那么就可以给被观察对象绑定一个Map,这个Map的作用是存储管理该对象被观察的属性,用它来维护对象的被观察属性的移除和添加;这样做的好处有以下两点:

  • 如果是非正常的添加或者删除观察者,就可以通过Map的存储判断出异常,从而避免这种操作;
  • 被观察者在dealloc之前都会销毁关联的对象,这时该Map也被自动销毁(系统特性,对象销毁的时候,会检查和该对象关联的对象,然后销毁),避免对象销毁时,还注册者观察者;

NSTimer引起的崩溃

开发时,我们不免使用定时器,在使用以下方法时:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

定时器会对target施加一个强引用,如果不在适当时机对timer进行invalidate,则会出现内存泄露;假如当对象销毁之后,还没有对定时器进行invalidate,则在某种情况下,也会引起崩溃;具体情况和selector内部实现有关;

在这里,对NSTimer的处理为,引入中间代理对象TimerTagetAgent,解除timertarget之间的循环引用;结构如下:

Timer.jpg

这样处理之后,解除掉了timertarget的强引用,并且可以在TimerTagetAgent中对timer进行适时的invalidate掉,这样就解决了内存泄露和不确定性闪退问题,并且可以上报错误,督促开发人员改正;

非主线程刷新UI

在非主线程刷新UI操作会导致界面不能按照想要的结果展示,而且很有可能造成崩溃;在这里处理方法为:HOOK以下下三个系统方法,在debugrelease模式下做不同操作;

- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;

debug模式下:使用断言机制,是程序进行崩溃,并输出错误信息,促使开发人员修改问题;
release模式下:异步到主线程刷新UI
这里和之前类型的崩溃采用的方法不同,是在开发环境下是程序主动崩溃,促使开发人员解决问题,在生产环境下采用崩溃保护,但是没有上报异常,原因为:这三个方法刷新UI的操作时在RunLoop的每个时钟周期都会操作,如果上报异常,服务器将会收到大量不必要信息;

野指针

NSNotification引起的崩溃

对于iOS 9以下需要做操作,但由于9以下系统较少,此模块后续完善;

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,690评论 0 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,092评论 1 32
  • 面向对象的三大特性:封装、继承、多态 OC内存管理 _strong 引用计数器来控制对象的生命周期。 _weak...
    运气不够技术凑阅读 1,092评论 0 10
  • 面试题参考1 : 面试题[http://www.cocoachina.com/ios/20150803/12872...
    江河_ios阅读 1,721评论 0 4
  • 最近在做课程设计,每天挺忙的,很多时候都没来得及看朋友过我发的消息,很久之后才回复朋友,他和我调侃说“看来你...
    念你有时阅读 418评论 0 1