美文网首页好文
iOS Crash防护系统-IronMan

iOS Crash防护系统-IronMan

作者: superFool | 来源:发表于2021-05-13 08:55 被阅读0次

    写在前面:数组越界这类的 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 <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防护方案

    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

        本文链接:https://www.haomeiwen.com/subject/hvgfjltx.html