开发过程中,即使我们很注意的去写代码,但是还是不能百分百的保证避免程序的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.以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制;
消息转发机制如下图:
在消息转发流程中,有三次机会可以“拯救”没有实现的方法引起的崩溃,分别再消息转发的三步流程中,我们可以通过HOOK这三步的方法实现对该类崩溃的保护;
1.+ (BOOL)resolveInstanceMethod:(SEL)sel
:obj
找不到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
,解除timer
和target
之间的循环引用;结构如下:
这样处理之后,解除掉了timer
对target
的强引用,并且可以在TimerTagetAgent
中对timer
进行适时的invalidate掉,这样就解决了内存泄露和不确定性闪退问题,并且可以上报错误,督促开发人员改正;
非主线程刷新UI
在非主线程刷新UI操作会导致界面不能按照想要的结果展示,而且很有可能造成崩溃;在这里处理方法为:HOOK以下下三个系统方法,在debug
和release
模式下做不同操作;
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
debug
模式下:使用断言机制,是程序进行崩溃,并输出错误信息,促使开发人员修改问题;
release
模式下:异步到主线程刷新UI
这里和之前类型的崩溃采用的方法不同,是在开发环境下是程序主动崩溃,促使开发人员解决问题,在生产环境下采用崩溃保护,但是没有上报异常,原因为:这三个方法刷新UI的操作时在RunLoop的每个时钟周期都会操作,如果上报异常,服务器将会收到大量不必要信息;
野指针
NSNotification
引起的崩溃
对于iOS 9以下需要做操作,但由于9以下系统较少,此模块后续完善;