写在前面:数组越界这类的 Crash 是最简单的也是最容易出现,业务开发过程中很可能操作某个 NSArray 类型的对象时忘记判空或者忘记长度判断而造成数组越界崩溃,所以最好是在线上环境接入这类的 Crash 防护。当然,在开发环境下最好不要接入,避免纵容开发者出现这类遗忘判断的错误。
另外线上接入了这类的防护之后要比前边的文章讲的 Unrecognized Selector Crash 和 EXC_BAD_ACCESS Crash 更容易造成业务逻辑的错乱,毕竟业务逻辑中不可避免的要用到大量的 NSArray、NSDictionary 类,可能在接入这类防护后会操成点击无响应或者页面卡死,有时候这种情况甚至比程序崩溃还让用户崩溃,所以也要看实际开发需要的取舍。在接入防护后尤其要做好堆栈收集,上报 Crash 的工作,及时解决掉问题。
一、背景
- App Crash会给用户造成很不好的用户体验,有时候会因为很小的问题导致Crash,而且有些跟业务流程无关的Crash还会阻塞业务的进展.
- 发现App Crash Bug是需要我们第一时间处理的,可能周末正在LOL或者在外面陪老婆孩子,Leader一个电话我们就要第一时间回去处理
- App Crash 可能是非常小的问题造成的,但是往往会被认定为线上严重问题从而对我们的绩效考核造成影响(当然最主要还是因为提升用户体验)
二、iOS App Crash类型
iOS App常见的Crash 类型:
- unrecognized selector crash(方法未实现)
- Container crash(数组越界,插nil等)
- NSTimer crash
- KVO crash
- NSNotification crash
- Bad Access crash (野指针)
- UI not on Main Thread Crash (非主线程刷UI)
三、ZCZYIronMan简介
- 目标:防护app里出现的前五种类型的Crash,并上报被防护住的crash
- 目前进度:2.0版本实现了unrecognized selector类型的Crash防护和容器类常用API的防护 NSTimer crash 的防护和KVO Crash的防护 由于iOS9之后苹果优化了NSNotification,所以不在对NSNotification做防护 目标已完成目标的90% 计划3.0版本加上线Crash日志符号化功能(由于上线的包都是去符号的,线上获取到的Crash调用栈信息需要符号化处理),防护代码正在整理中后期会放到github上开源。
- 集成: 直接使用pod 'IronMan'引用项目即可不需要其他配置(当然源码是放在我们私有pod库的,外部是无法使用的)
四、原理介绍
4.1 unrecognized selector防护
4.1.1.unrecognized selector Crash是怎么出现的
这类Crash出现的频率还是比较高的,是因为对象调用没有实现的方法造成的,要弄清楚这类Crash出现的具体原因需要对方法调用过程有一定的了解。
下面我们来看一下方法调用时Runtime大致做了些什么:
1.首先通过对象的isa指针找到对象的类Class
2.在Class的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
3.如果没找到,在Class的方法列表中找调用的方法,如果找到,转向相应实现执行
4.如果没找到,去父类指针所指向的对象中执行2,3.
5.以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制。
6.如果没有重写拦截调用的方法,程序报错。
4.1.2 防护方案选型
发生unrecognized selector Crash之前系统会给三次挽回的机会,这三次机会就在上面方法调用第5步消息转发流程里,下面我们来了解一下消息转发。(要先对iOS的消息机制有一定了解,才能更好理解消息转发)
消息转发的三大步骤:消息动态解析、消息接受者重定向、消息重定向。通过这三大步骤,可以让我们在程序找不到调用方法崩溃之前,拦截方法调用,每一步对应一个防护方案。
大致流程如下(消息转发详细流程:传送门):
1、消息动态解析:Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。我们可以通过重写这两个方法,添加其他函数实现,并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。若返回 NO 或者没有添加其他函数实现,则进入下一步。
2、消息接受者重定向:如果当前对象实现了 forwardingTargetForSelector:,Runtime 就会调用这个方法,允许我们将消息的接受者转发给其他对象。如果这一步方法返回 nil,则进入下一步。
3、消息重定向:Runtime 系统利用 methodSignatureForSelector: 方法获取函数的参数和返回值类型。
如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了
这三步都可以拦截做防护那我们怎么选择呢
-
resolveInstanceMethod: 会为对象或类新增一个方法。如果此时这个类是个系统原生的类,比如 NSArray ,你向他发送了一条 setValue: forKey: 的方法,这本身就是一次错发。此时如果你为他添加这个方法,这个方法一般来说就是冗余的。
-
forwardInvocation: 必须要经过 methodSignatureForSelector: ** 方法来获得一个NSInvocation,开销比较大。苹果在 forwardingTargetForSelector **的discussion中也说这个方法是一个相对开销多的多的方法。
-
forwardingTargetForSelector: 这个方法目的单纯,就是转发给另一个对象,别的他什么都不干,相对以上两个方法,更适合重写。
既然** forwardingTargetForSelector: **方法能够转发给别其他对象,那我们可以创建一个类,所有的没查找到的方法全部转发给这个类,由他来动态的实现。而这个类中应该有一个安全的实现方法来动态的代替原方法的实现。
4.1.3 最终的防护方案
防护流程:
1、对NSObject的forwardingTargetForSelector进行hook
2、当forwardingTargetForSelector:消息重定向触发的时候判断当前类自己有没有实现消息转发,如果实现了就走当前类的消息转发。
3、当前类没有实现消息转发就动态创建一个类,添加当前调用的方法,把消息转发给这个类处理
具体实现:
#import "NSObject+IMNIronMan.h"
#import "NSObject+IMNMethodSwizzling.h"
#import <objc/runtime.h>
@implementation NSObject (IMNIronMan)
+ (void)load {
static dispatch_once_t onceToken;
//防止重复的方法交换
dispatch_once(&onceToken, ^{
// 拦截 `+forwardingTargetForSelector:` 方法,替换自定义实现
[NSObject IMNIronManSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(ironMan_forwardingTargetForSelector:)
withClass:[NSObject class]];
// 拦截 `-forwardingTargetForSelector:` 方法,替换自定义实现
[NSObject IMNIronManSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(ironMan_forwardingTargetForSelector:)
withClass:[NSObject class]];
});
}
// 自定义实现 `+ironMan_forwardingTargetForSelector:` 方法
+ (id)ironMan_forwardingTargetForSelector:(SEL)aSelector {
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 获取 NSObject 的消息转发方法
Method origin_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
// 获取 当前类 的消息转发方法
Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);
// 判断当前类本身是否实现第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(origin_forwarding_method);
// 如果没有实现第二步:消息接受者重定向
if (!realize) {
// 判断有没有实现第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method origin_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(origin_methodSignature_method);
// 如果没有实现第三步:消息重定向
if (!realize) {
// 创建一个新类
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSLog(@"*** Crash Message: +[%@ %@]: unrecognized selector sent to class %p ***",errClassName, errSel, self);
NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);
// 如果类不存在 动态创建一个类
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注册类
objc_registerClassPair(cls);
}
// 如果类没有对应的方法,则动态添加一个
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
}
// 把消息转发到当前动态生成类的实例对象上
return [[cls alloc] init];
}
}
return [self ironMan_forwardingTargetForSelector:aSelector];
}
// 自定义实现 `-ironMan_forwardingTargetForSelector:` 方法
- (id)ironMan_forwardingTargetForSelector:(SEL)aSelector {
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 获取 NSObject 的消息转发方法
Method origin_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
// 获取 当前类 的消息转发方法
Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
// 判断当前类本身是否实现第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(origin_forwarding_method);
// 如果没有实现第二步:消息接受者重定向
if (!realize) {
// 判断有没有实现第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method origin_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(origin_methodSignature_method);
// 如果没有实现第三步:消息重定向
if (!realize) {
//打印防护日志
logStakSymbols(self,aSelector);
// 创建一个新类
NSString *className = @"IronMan";
Class cls = NSClassFromString(className);
// 如果类不存在 动态创建一个类
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注册类
objc_registerClassPair(cls);
}
// 如果类没有对应的方法,则动态添加一个
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
}
// 把消息转发到当前动态生成类的实例对象上
return [[cls alloc] init];
}
}
return [self ironMan_forwardingTargetForSelector:aSelector];
}
// 动态添加的方法实现
static int Crash(id slf, SEL selector) {
return 0;
}
//打印调用栈信息
void logStakSymbols(id self,SEL aSelector){
NSString *selectorStr = NSStringFromSelector(aSelector);
NSLog(@"IronMan: -[%@ %@]", [self class], selectorStr);
NSLog(@"IronMan: unrecognized selector \"%@\" sent to instance: %p", selectorStr, self);
// 查看调用栈
NSLog(@"IronMan: call stack: \n%@", [NSThread callStackSymbols]);
}
@end
参考资料:
iOS 开发:『Runtime』详解(一)基础知识
iOS 开发:『Crash 防护系统』(一)Unrecognized Selector
iOS中对unrecognized selector的防御
大白健康系统--iOS APP运行时Crash自动修复系统
4.2 Container Crash防护(NSArray,NSMutableArray,NSDictionary)
4.2.1.Container Crash是什么
容器类的Crash也是比较常见的,例如:给NSMutableArray插入nil、数组越界、初始化NSDictonary时数据中有nil等。NSArray 调用addObject:方法Crash不属于此类型,而是属于unrecognized selector
4.2.2 防护方案选型
这种类型Crash的防护业内常用的有两种:
- 一种是hook常用的API,每个API中都加入
try/catch
- 一种是hook常用的API,做容错处理
第一种方法的好处是可以直接调用原来的API实现如果try/catch
没有捕获到异常就不用做容错操作,发生异常执行容错操作,但是坏处也很突出就是try/catch
本身的开销太大了得不偿失。
第二种方法的坏处是每次都需要执行容错操作,但是好处是容错操作的开销并不会太大,可以接受
4.2.3 最终的防护方案
选中第二种方案
流程:
1、找到需要防护的容器类(由于NSArray、NSDictionary等都是类簇需要找到运行时实际的类)
2、hook常用的API,做容错处理
下面就以NSArray举例,其他容器类同理直接看代码就行
/**
iOS 8:下都是__NSArrayI
iOS11: 之后分 __NSArrayI、 __NSArray0、__NSSingleObjectArrayI
iOS11之前:arr@[] 调用的是[__NSArrayI objectAtIndexed]
iOS11之后:arr@[] 调用的是[__NSArrayI objectAtIndexedSubscript]
arr为空数组
*** -[__NSArray0 objectAtIndex:]: index 12 beyond bounds for empty NSArray
arr只有一个元素
*** -[__NSSingleObjectArrayI objectAtIndex:]: index 12 beyond bounds [0 .. 0]
*/
#import "NSArray+IMNIronMan.h"
#import <objc/runtime.h>
#import "NSObject+IMNMethodSwizzling.h"
@implementation NSArray (IMNIronMan)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/**
__NSArray0 仅仅初始化后不含有元素的数组 ( NSArray *arr2 = [[NSArray alloc]init]; )
__NSSingleObjectArrayI 只有一个元素的数组 ( NSArray *arr3 = [[NSArray alloc]initWithObjects: @"1",nil]; )
__NSPlaceholderArray 占位数组 ( NSArray *arr4 = [NSArray alloc]; ) 最后会被替换成另外三个类,所以不用swizzing
__NSArrayI 初始化后的不可变数组 ( NSArray *arr1 = @[@"1",@"2"]; )
*/
// Class __NSArray = objc_getClass("NSArray");
Class __NSArrayI = objc_getClass("__NSArrayI");
Class __NSSingleObjectArrayI = objc_getClass("__NSSingleObjectArrayI");
Class __NSArray0 = objc_getClass("__NSArray0");
SEL origin_arrayWithObjects = @selector(arrayWithObjects:count:);
SEL origin_objectAtIndex = @selector(objectAtIndex:);
SEL origin_objectAtIndexedSubscript = @selector(objectAtIndexedSubscript:);
SEL my_arrayWithObjects = @selector(ironMan_arrayWithObjects:count:);
//__NSArray0
SEL my_objectAtIndexForEmptyArray = @selector(ironMan_objectAtIndexForEmptyArray:);
SEL my_objectAtIndexedForEmptyArraySubscript = @selector(ironMan_objectAtIndexedForEmptyArraySubscript:);
//__NSSingleObjectArrayI
SEL my_objectAtIndexForSingleObjectArray = @selector(ironMan_objectAtIndexForSingleObjectArray:);
SEL my_objectAtIndexedForSingleObjectArraySubscript = @selector(ironMan_objectAtIndexedForSingleObjectArraySubscript:);
//__NSArrayI
SEL my_objectAtIndex = @selector(ironMan_objectAtIndex:);
SEL my_objectAtIndexedSubscript = @selector(ironMan_objectAtIndexedSubscript:);
// 含多个object数组 arr = @[@"",@""] [arr objectAtIndex:] arr[]
[self IMNIronManSwizzlingClassMethod:origin_arrayWithObjects withMethod:my_arrayWithObjects withClass:__NSArrayI];
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndex withMethod:my_objectAtIndex withClass:__NSArrayI];
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndexedSubscript withMethod:my_objectAtIndexedSubscript withClass:__NSArrayI];
//空数组 [arr objectAtIndex:] arr[]
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndex withMethod:my_objectAtIndexForEmptyArray withClass:__NSArray0];
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndexedSubscript withMethod:my_objectAtIndexedForEmptyArraySubscript withClass:__NSArray0];
//只含一个object数组 [arr objectAtIndex:] arr[]
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndex withMethod:my_objectAtIndexForSingleObjectArray withClass:__NSSingleObjectArrayI];
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndexedSubscript withMethod:my_objectAtIndexedForSingleObjectArraySubscript withClass:__NSSingleObjectArrayI];
});
}
+ (instancetype)ironMan_arrayWithObjects:(id _Nonnull const [])objects count:(NSUInteger)cnt{
NSUInteger newCnt = 0;
for (NSUInteger i = 0; i < cnt; i++) {
if (!objects[i]) {
break;
}
newCnt++;
}
return [self ironMan_arrayWithObjects:objects count:newCnt];
}
//__NSArray0 空数组
- (id)ironMan_objectAtIndexForEmptyArray:(NSUInteger)index{
return nil;
}
- (id)ironMan_objectAtIndexedForEmptyArraySubscript:(NSUInteger)idx{
return nil;
}
//__NSSingleObjectArrayI 只有包含一个object的数组
- (id)ironMan_objectAtIndexForSingleObjectArray:(NSUInteger)index{
if ( index >= 1) {
arrayLogStakSymbols(self,_cmd,index,1);
return nil;
}
return [self ironMan_objectAtIndexForSingleObjectArray:index];
}
- (id)ironMan_objectAtIndexedForSingleObjectArraySubscript:(NSUInteger)idx{
if (idx >= 1) {
arrayLogStakSymbols(self,_cmd,idx,1);
return nil;
}
return [self ironMan_objectAtIndexedForSingleObjectArraySubscript:idx];
}
//__NSArrayI
- (id)ironMan_objectAtIndex:(NSUInteger)index{
if ( index >= self.count) {
arrayLogStakSymbols(self,_cmd,index,self.count);
return nil;
}
return [self ironMan_objectAtIndex:index];
}
- (id)ironMan_objectAtIndexedSubscript:(NSUInteger)idx{
if (idx >= self.count) {
arrayLogStakSymbols(self,_cmd,idx,self.count);
return nil;
}
return [self ironMan_objectAtIndexedSubscript:idx];
}
//打印调用栈信息
void arrayLogStakSymbols(id self,SEL aSelector,long index,long length){
NSString *selectorStr = NSStringFromSelector(aSelector);
NSLog(@"IronMan:container Crash Bombing");
NSLog(@"IronMan: -[%@ %@]: index %ld beyond bounds [0 .. %ld]", [self class], selectorStr,index,length - 1);
// 查看调用栈
NSLog(@"IronMan: call stack: \n%@", [NSThread callStackSymbols]);
}
@end
容器类运行时实际的类型
- (void)test{
// NSArray
NSLog(@"arr alloc:%@", [NSArray alloc].class); // __NSPlaceholderArray
NSLog(@"arr init:%@", [[NSArray alloc] init].class); // __NSArray0
NSLog(@"arr:%@", [@[] class]); // __NSArray0
NSLog(@"arr:%@", [@[@1] class]); // __NSSingleObjectArrayI
NSLog(@"arr:%@", [@[@1, @2] class]); // __NSArrayI
// NSMutableArray
NSLog(@"mutA alloc:%@", [NSMutableArray alloc].class); // __NSPlaceholderArray
NSLog(@"mutA init:%@", [[NSMutableArray alloc] init].class); // __NSArrayM
NSLog(@"mutA:%@", [@[].mutableCopy class]); // __NSArrayM
NSLog(@"mutA:%@", [@[@1].mutableCopy class]); // __NSArrayM
NSLog(@"mutA:%@", [@[@1, @2].mutableCopy class]); // __NSArrayM
// NSDictionary
NSLog(@"dict alloc:%@", [NSDictionary alloc].class); // __NSPlaceholderDictionary
NSLog(@"dict init:%@", [[NSDictionary alloc] init].class); // __NSDictionary0
NSLog(@"dict:%@", [@{} class]); // __NSDictionary0
NSLog(@"dict:%@", [@{@1:@1} class]); // __NSSingleEntryDictionaryI
NSLog(@"dict:%@", [@{@1:@1, @2:@2} class]); // __NSDictionaryI
// NSMutableDictionary
NSLog(@"mutD alloc:%@", [NSMutableDictionary alloc].class); // __NSPlaceholderDictionary
NSLog(@"mutD init:%@", [[NSMutableDictionary alloc] init].class); // __NSDictionaryM
NSLog(@"mutD:%@", [@{}.mutableCopy class]); // __NSDictionaryM
NSLog(@"mutD:%@", [@{@1:@1}.mutableCopy class]); // __NSDictionaryM
NSLog(@"mutD:%@", [@{@1:@1, @2:@2}.mutableCopy class]); // __NSDictionaryM
// NSString
NSLog(@"str:%@", [@"" class]); // __NSCFConstantString
// NSNumber
NSLog(@"num:%@", [@1 class]); // __NSCFNumber
}
参考资料:
iOS崩溃处理机制:Container类型crash防护
Crash 防护方案(三):Container (NSArray、NSDictionary、NSNumber etc.)
大白健康系统--iOS APP运行时Crash自动修复系统
4.3 NSTimer Crash 防护
4.3.1 NSTimer 的问题
我们平常的开发中经常用到NSTimer,但是NSTimer有个大坑一不小心就会遇到问题,一般我们会这样使用NSTimer.
@interface TimerVC ()
@property(nonatomic, strong)NSTimer *timer;
@end
@implementation TimerVC
- (void)viewDidLoad {
[super viewDidLoad];
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction{
count += 1;
NSLog(@"count: %@",@(count));
}
- (void)dealloc
{
NSLog(@"%s",__func__);
[self.timer invalidate];
}
@end
声明一个属性持有timer,在self的dealloc里执行invalidate,看似没没什问题,但是NSTimer的scheduledTimerWithTimeInterval: target: selector: userInfo:nil repeats:`会让timer会强引用Target,而Targer又通过timer属性持有timer,这样就形成了循环引用,self和timer都不会被释放,self的dealloc就不会执行,timer会一直执行,造成内存泄漏,甚至在定时任务触发时导致crash。 crash的展现形式和具体的target执行的selector有关。
与此同时,如果NSTimer是无限重复的执行一个任务的话,也有可能导致target的selector一直被重复调用且处于无效状态,对app的CPU,内存等性能方面均是没有必要的浪费。
4.3.2 NSTimer Crash 防护方案
解决这类Crash的关键就在于如何打破这个保留环,网上流行的方案又3种
1. 在合适的时机手动释放timer
这种方案太low了一点也不优雅就不用过多介绍了
2.1 给NSTimer 添加一个block,把NSTimer的Target设置成timer自己,当定时器事件触发时调用block,这样由于Target发生了变化,原来的保留环被打破,使得原来的Target可以正常的释放,虽然没有了循环引用,但是还是应该记得在dealloc时释放timer。
@implementation NSTimer (ActionBlock)
+ (NSTimer *)ab_scheduledTimerWithTimeInterval:(NSTimeInterval)ti block:(void(^)(void))block{
return [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(timerAction:) userInfo:[block copy] repeats:YES];
}
- (void)timerAction:(NSTimer *)timer{
void(^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
调用
_timer = [NSTimer ab_scheduledTimerWithTimeInterval:1 block:^{
NSLog(@"timerBlock");
}];
这样确实可以打破保留环,但是需要我们用使用自定义的APIab_scheduledTimerWithTimeInterval:block:
老项目还得替换API,而且如果不小心调用了系统的API还是会有问题,还是不够优雅,那我们就对这个方案改进一下.
2.2 使用Method Swizzling 配合 block
废话不多说直接上代码
@implementation NSTimer (ActionBlock)
+ (void)load {
static dispatch_once_t onceToken;
//防止重复的方法交换
dispatch_once(&onceToken, ^{
Method imp = class_getInstanceMethod(object_getClass([self class]), @selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:));
Method myImp = class_getInstanceMethod(object_getClass([self class]), @selector(my_scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:));
method_exchangeImplementations(imp, myImp);
});
}
+ (NSTimer *)my_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
__weak typeof(aTarget) target = aTarget;
void(^block)(void) = ^{
if ([target respondsToSelector:aSelector]) {
[target performSelector:aSelector];
}
};
return [NSTimer my_scheduledTimerWithTimeInterval:ti target:self selector:@selector(timerAction:) userInfo:[block copy] repeats:YES];
}
+ (void)timerAction:(NSTimer *)timer{
void(^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
交换系统API 在自定义的方法中使用block 并且使用weak调用原来target的selector 由于使用了weak不会造成循环引用,而且也可以直接使用系统的API,是不是很完美?但是还有一个小问题我们这里只用了userInfo来传递block,这样如果需要用userInfo传递数据时就会有问题,下来请出第三种方案
3. 添加代理
添加一个代理IMNTimerProxy 类,用它作为NSTimer新的Target,而这个类弱引用原来的Target,通过消息转发将timer的执行方法转发给原来的Target,这样就打破了原有的循环引用。
2806916-9310b37f6734bde6.png.jpeg
上代码
@implementation NSTimer (ActionBlock)
+ (void)load {
static dispatch_once_t onceToken;
//防止重复的方法交换
dispatch_once(&onceToken, ^{
Method imp = class_getInstanceMethod(object_getClass([self class]), @selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:));
Method myImp = class_getInstanceMethod(object_getClass([self class]), @selector(my_scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:));
method_exchangeImplementations(imp, myImp);
});
}
+ (NSTimer *)my_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
IMNTimerProxy *proxyObjc = [IMNTimerProxy proxyWithWeakObject:aTarget];
NSTimer * timer = [self my_scheduledTimerWithTimeInterval:ti target:proxyObjc selector:aSelector userInfo:userInfo repeats:yesOrNo];
return timer;
}
@end
设置代理对象proxyObjc为NSTimer的target
@interface IMNTimerProxy : NSObject
@property (weak, nonatomic) id weakObject;
- (instancetype)initWithWeakObject:(id)obj;
+ (instancetype)proxyWithWeakObject:(id)obj;
@end
@implementation IMNTimerProxy
- (instancetype)initWithWeakObject:(id)obj {
_weakObject = obj;
return self;
}
+ (instancetype)proxyWithWeakObject:(id)obj {
return [[IMNTimerProxy alloc] initWithWeakObject:obj];
}
/**
* 消息转发,对象转发,让_weakObject响应事件
*/
- (id)forwardingTargetForSelector:(SEL)aSelector {
return _weakObject;
}
@end
Proxy
中弱引用obj,再通过消息转发,把timer执行的方法转发给原来的obj对象,这种方式解决了之前所有的问题。不过也要记得在obj的dealloc方法中释放timer。
参考资料:
NSTimer循环引用的几种解决方案
大白健康系统--iOS APP运行时Crash自动修复系统
4.4 KVO Crash 防护方案
4.4.1 KVO Crash 出现的原因
KVO API设计非常不合理,使用时一不小心就会造成Crash,此类Crash主要是因为观察者在销毁之后没有移除KVO,添加KVO重复添加观察者或重复移除观察者(KVO 注册观察者与移除观察者不匹配)导致的crash。
4.4.2 KVO Crash防护方案
- 有很多的KVO三方库,比如 KVOController 用更优的API来规避这些crash,但是侵入性比较大,必须编码规范来约束所有人都要使用该方式。
2.像网易推出的大白健康系统
KVO的被观察者dealloc时仍然注册着KVO导致的crash 的情况,可以将NSObject的dealloc swizzle, 在object dealloc的时候自动将其对应的kvodelegate所有和kvo相关的数据清空,然后将kvodelegate也置空。避免出现KVO的被观察者dealloc时仍然注册着KVO而产生的crash
这种方式也是可以的,可以完全避免KVO Crash的出现但是太过麻烦了。
3.可以考虑建立一个哈希表,用来保存观察者、keyPath的信息,如果哈希表里已经有了相关的观察者,keyPath信息,那么继续添加观察者的话,就不载进行添加,同样移除观察的时候,也现在哈希表中进行查找,如果存在观察者,keypath信息,那么移除,如果没有的话就不执行相关的移除操作。要实现这样的思路就需要用到methodSwizzle来进行方法交换。我这通过写了一个NSObject的cagegory来进行方法交换。
需要交换
addObserver:forKeyPath:options:context:
removeObserver:forKeyPath:
-
removeObserver:forKeyPath:context:
这三个方法
首先在load方法里做方法交换
@implementation NSObject (KVOCrash)
+ (void)load {
static dispatch_once_t onceToken;
//防止重复的方法交换
dispatch_once(&onceToken, ^{
SEL origin_addObserver = @selector(addObserver:forKeyPath:options:context:);
SEL origin_removeObserver = @selector(removeObserver:forKeyPath:);
SEL origin_removeObserverContext = @selector(removeObserver:forKeyPath:context:);
SEL ironMan_addObserver = @selector(ironMan_addObserver:forKeyPath:options:context:);
SEL ironMan_removeObserver = @selector(ironMan_removeObserver:forKeyPath:);
SEL ironMan_removeObserverContext = @selector(ironMan_removeObserver:forKeyPath:context:);
[NSObject IMNIronManSwizzlingClassMethod:origin_addObserver
withMethod:ironMan_addObserver
withClass:[NSObject class]];
[NSObject IMNIronManSwizzlingClassMethod:origin_removeObserver
withMethod:ironMan_removeObserver
withClass:[NSObject class]];
[NSObject IMNIronManSwizzlingClassMethod:origin_removeObserverContext
withMethod:ironMan_removeObserverContext
withClass:[NSObject class]];
});
}
//使用关联对象创建hash表
- (NSHashTable *)KVOHashTable{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setKVOHashTable:(NSHashTable *)KVOHashTable{
objc_setAssociatedObject(self, @selector(KVOHashTable), KVOHashTable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
- 在自定义的
ironMan_addObserver
方法里把KVO对应的hash值存在hash表中然后调用系统的addObserver
(由于已经方法交换过了所以还是调用ironMan_addObserver
)方法
然后再观察者和被观察者即将销毁时移除对应的kvo(这里使用了CYLDeallocBlockExecutor
三方库来监听对象的销毁) - 先判断hash表中是否保存过对应的hashKey,如果之前添加过就不在进行后续操作了避免重复添加
- hash表是用关联对象保存的
- (void)ironMan_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
...省略检测代码
@synchronized (self) {
NSString * kvoHash = [self hashKeyWithObserver:observer keyPath:keyPath];
if (!self.KVOHashTable) {
self.KVOHashTable = [NSHashTable hashTableWithOptions:NSPointerFunctionsStrongMemory];
}
if (![self.KVOHashTable containsObject:kvoHash]) {
[self.KVOHashTable addObject:kvoHash];
[self ironMan_addObserver:observer forKeyPath:keyPath options:options context:context];
__weak typeof(observer) weakObserver = observer;
[self cyl_willDeallocWithSelfCallback:^(__unsafe_unretained id observedOwner, NSUInteger identifier) {
[observedOwner ironMan_removeObserver:weakObserver forKeyPath:keyPath context:context];
}];
__weak typeof(self) unsafeUnretainedSelf = self;
[observer cyl_willDeallocWithSelfCallback:^(__unsafe_unretained id observerOwner, NSUInteger identifier) {
[unsafeUnretainedSelf ironMan_removeObserver:observerOwner forKeyPath:keyPath context:context];
}];
}
}
}
-
ironMan_removeObserver
方法在remove之前先校验hash表里是否有KVO对应的hash值有的话才移除,没有的话就不移除,避免重复移除
- (void)ironMan_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
...省略校验代码
@synchronized (self) {
if (!observer) {
return;
}
NSString * kvoHash = [self hashKeyWithObserver:observer keyPath:keyPath];
NSHashTable *hashTable = [self KVOHashTable];
if (!hashTable) {
return;
}
if ([hashTable containsObject:kvoHash]) {
[self ironMan_removeObserver:observer forKeyPath:keyPath];
[hashTable removeObject:kvoHash];
}
}
}
- (void)ironMan_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context{
[self removeObserver:observer forKeyPath:keyPath];
}
近期整理一下代码准备上传到github上开源,敬请期待~
参考资料:
iOS KVO crash 自修复技术实现与原理解析
大白健康系统--iOS APP运行时Crash自动修复系统
网友评论