iOS Crash防护系统-IronMan

写在前面:数组越界这类的 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的消息机制有一定了解,才能更好理解消息转发)

消息转发的三大步骤:消息动态解析消息接受者重定向消息重定向。通过这三大步骤,可以让我们在程序找不到调用方法崩溃之前,拦截方法调用,每一步对应一个防护方案。
大致流程如下(消息转发详细流程:传送门):

消息转发.png

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 

@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 
#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防护方案

  1. 有很多的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自动修复系统

你可能感兴趣的:(iOS Crash防护系统-IronMan)