美文网首页
iOS的Exception类型与防护

iOS的Exception类型与防护

作者: YanZi_33 | 来源:发表于2021-01-26 19:28 被阅读0次

第一类: 找不到方法的实现unrecognized selector sent to instance

1. 现象及原理剖析

给某个对象发送消息,但对象找不到方法的实现且没有做消息转发机制的处理,从而引起程序的崩溃.

首先我们来说说消息的转发机制

给一个对象发送消息,对象会根据自己的isa指针找到自己所属的类class,

  • 从类的方法缓存列表(method cache list),寻找 命中直接调用;
  • 未命中,根据继承链从父类中寻找,命中直接调用,并存入方法缓存列表;未命中一直递归到根类NSObject;依旧没有寻找到,此刻正式进入消息的转发阶段(分为三个阶段);
  • 第一个阶段: 动态方法解析 其本质是当前类添加一个方法实现,然后利用RunTime将这个方法实现动态添加给需要转发的消息.
+ (BOOL)resolveInstanceMethod:(SEL)sel;
  • 第二个阶段: 消息的重定向将消息转发给另外一个对象 其本质是给需要转发的消息提供一个消息的接受者,并且消息接受者实现了转发消息的实现.
- (id)forwardingTargetForSelector:(SEL)aSelector;
  • 第三个阶段: 完整的消息重定向 其本质与第二个阶段相同,但其更完整不仅提供消息的接收者且提供方法的签名.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
  • 消息转发机制,三个步骤均没有处理,会导致程序崩溃出现 unrecognized selector sent to instance的报错.
2. 防护的实现

知道了消息转发机制的具体流程,我们选择在第二个阶段(消息的重定向)进行拦截,替换掉系统的方法,换成自己实现的消息重定向方法.

方法交换封装在一个NSObject分类中:

/// 交换实例方法的实现
void kExceptionMethodSwizzling(Class clazz, SEL original, SEL swizzled){
    Method originalMethod = class_getInstanceMethod(clazz, original);
    Method swizzledMethod = class_getInstanceMethod(clazz, swizzled);
    if (class_addMethod(clazz, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(clazz, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

/// 交换类方法的实现
void kExceptionClassMethodSwizzling(Class clazz, SEL original, SEL swizzled){
    Method originalMethod = class_getClassMethod(clazz, original);
    Method swizzledMethod = class_getClassMethod(clazz, swizzled);
    Class metaclass = objc_getMetaClass(NSStringFromClass(clazz).UTF8String);
    if (class_addMethod(metaclass, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(metaclass, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

为什么选择在第二个阶段(消息的重定向)进行拦截?

  • 第一个阶段给当前类添加一个方法,这个方法对于当前类来说是冗余的;
  • 第二个阶段与第三个阶段的本质是相同的,都是将消息转发给另外一个对象,但第三个阶段更完整,则开销会比较大并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写.

自己实现的消息重定向方法 具体代码如下:

- (id)yy_forwardingTargetForSelector:(SEL)aSelector{
    //消息转发机制的第二个阶段 将消息转发给其他对象
    SEL forwarding_sel = @selector(forwardingTargetForSelector:);
    
    //获取 NSObject 的消息转发方法
    Method root_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(root_forwarding_method);
    
    //如果没有实现第二步:消息接受者重定向
    if (!realize) {
        //判断有没有实现第三步:消息重定向
        SEL methodSignature_sel = @selector(methodSignatureForSelector:);
        Method root_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(root_methodSignature_method);
        
        //如果没有实现第三步:消息重定向
        if (!realize) {
            //创建一个新类
            NSString *errClassName = NSStringFromClass([self class]);
            NSString *errSel = NSStringFromSelector(aSelector);
            NSLog(@"出问题的类,出问题的对象方法 == %@ %@", errClassName, errSel);
            
            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 yy_forwardingTargetForSelector:aSelector];
}

//动态添加的方法实现
static int Crash(id slf, SEL selector) {
    NSLog(@"%s",__func__);
    return 0;
}

第二类: 容器类的相关崩溃

  • 场景一: 针对不可变数组 数组越界与数组为空

要注意的是容器类是类簇,直接hook容器类是无法成功的,需要hook对外隐藏的实际起作用的类,比如NSArray的实际类名为__NSArrayI; NSMutableArray的实际类名为__NSArrayM

//数组越界与数组为空
- (void)container_01{
    NSArray *arr = @[@"1",@"2",@"3",@"4",@"5"];
    //1> 取数组元素时越界 
    //reason:'-[__NSArrayI objectAtIndex:]: index 5 beyond bounds [0 .. 4]'
    NSString *str = [arr objectAtIndex:5];
    //2> 取数组元素时越界  
    //reason:'-[__NSArrayI objectAtIndexedSubscript:]: index 5 beyond bounds [0 .. 4]
    NSString *str1 = arr[5];
    NSLog(@" str = %@ , str1 = %@",str,str1);
    
    NSArray *arr1 = [[NSArray alloc]init];
    //3> 数组为空时 取元素 
    //reason:'-[__NSArray0 objectAtIndex:]: index 2 beyond bounds for empty NSArray'
    NSString *str2 = [arr1 objectAtIndex:2];
    NSLog(@" str2 = %@",str2);
}

防护代码如下所示: NSArray添加分类

+ (void)yy_openCrashExchangeMethod{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class __NSArrayI = objc_getClass("__NSArrayI");
        ///取元素时越界
        kExceptionMethodSwizzling(__NSArrayI, @selector(objectAtIndex:), @selector(yy_objectAtIndex:));
        ///取元素时越界
        kExceptionMethodSwizzling(__NSArrayI, @selector(objectAtIndexedSubscript:), @selector(yy_objectAtIndexedSubscript:));
        ///空数组取元素
        Class __NSArray0 = objc_getClass("__NSArray0");
        kExceptionMethodSwizzling(__NSArray0, @selector(objectAtIndex:), @selector(yy_objectAtIndexedNullArray:));
    });
}

- (instancetype)yy_objectAtIndex:(NSUInteger)index{
    NSArray *temp = nil;
    @try {
        temp = [self yy_objectAtIndex:index];
    }@catch (NSException *exception) {
        NSString *string = @" LYY crash: ";
        if (self.count == 0) {
            string = [string stringByAppendingString:@"数组个数为零"];
        }else if (self.count <= index) {
            string = [string stringByAppendingString:@"数组索引越界"];
        }
        [YYCrashManager yy_crashDealWithException:exception CrashTitle:string];
    }@finally {
        return temp;
    }
}

- (instancetype)yy_objectAtIndexedSubscript:(NSUInteger)index{
    NSArray *temp = nil;
    @try {
        temp = [self yy_objectAtIndexedSubscript:index];
    }@catch (NSException *exception) {
        NSString *string = @" LYY crash:";
        if (self.count == 0) {
            string = [string stringByAppendingString:@"数组个数为零"];
        }else if (self.count <= index) {
            string = [string stringByAppendingString:@"数组索引越界"];
        }
        [YYCrashManager yy_crashDealWithException:exception CrashTitle:string];
    }@finally {
        return temp;
    }
}

- (instancetype)yy_objectAtIndexedNullArray:(NSUInteger)index{
    id object = nil;
    @try {
        object = [self yy_objectAtIndexedNullArray:index];
    }@catch (NSException *exception) {
        NSString *string = @" LYY crash:";
        if (self.count == 0) {
            string = [string stringByAppendingString:@"数组个数为零"];
        }else if (self.count <= index) {
            string = [string stringByAppendingString:@"数组索引越界"];
        }
        [YYCrashManager yy_crashDealWithException:exception CrashTitle:string];
    }@finally {
        return object;
    }
}
  • 场景二: 针对可变数组的增删改查
- (void)container_02{
    NSMutableArray *mArr = [NSMutableArray new];
    [mArr addObject:@"1"];
    [mArr addObject:@"2"];
    [mArr addObject:@"3"];
    [mArr addObject:@"4"];
    [mArr addObject:@"5"];
    //1>移除元素时越界
    //reason:'-[__NSArrayM removeObjectsInRange:]: range {5, 1} extends beyond bounds [0 .. 4]'
    [mArr removeObjectAtIndex:5];
    
    //2>插入元素时越界
    //reason:'-[__NSArrayM insertObject:atIndex:]: index 10 beyond bounds [0 .. 4]'
    [mArr insertObject:@"6" atIndex:10];
 
    //3>更改元素时越界
    //reason: '-[__NSArrayM setObject:atIndexedSubscript:]: index 10 beyond bounds [0 .. 4]'
    [mArr setObject:@"5.5" atIndexedSubscript:10];
    
    //4>取元素时越界
    //reason: '-[__NSArrayM objectAtIndex:]: index 10 beyond bounds [0 .. 4]'
    [mArr objectAtIndex:10];
    
    //5>取元素时越界
    //reason: '-[__NSArrayM objectAtIndexedSubscript:]: index 10 beyond bounds [0 .. 4]'
    NSString *str = mArr[10];
    NSLog(@" str = %@",str);
    
    //6>初始化时有空对象nil
    //reason:'-[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[1]'
    NSString *strings[3];
    strings[0] = @"First";
    strings[1] = nil;
    strings[2] = @"Third";
    NSMutableArray *mArr2 = [[NSMutableArray alloc]initWithObjects:strings count:3];
    NSLog(@" mArr2 = %@",mArr2);
}

防护代码如下: NSMutableArray添加分类

+ (void)yy_openCrashExchangeMethod{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class __NSArrayM = objc_getClass("__NSArrayM");
        ///移除元素越界
        kExceptionMethodSwizzling(__NSArrayM, @selector(removeObjectAtIndex:), @selector(yy_removeObjectAtIndex:));
        ///新增元素越界
        kExceptionMethodSwizzling(__NSArrayM, @selector(insertObject:atIndex:), @selector(yy_insertObject:atIndex:));
        ///修改元素越界
        kExceptionMethodSwizzling(__NSArrayM, @selector(setObject:atIndexedSubscript:), @selector(yy_setObject:atIndexedSubscript:));
        ///取元素时越界
        kExceptionMethodSwizzling(__NSArrayM, @selector(objectAtIndex:), @selector(yy_objectAtIndex:));
        ///取元素时越界
        kExceptionMethodSwizzling(__NSArrayM, @selector(objectAtIndexedSubscript:), @selector(yy_objectAtIndexedSubscript:));
        ///初始化时有nil对象
        Class __NSPlaceholderArray = objc_getClass("__NSPlaceholderArray");
        kExceptionMethodSwizzling(__NSPlaceholderArray, @selector(initWithObjects:count:), @selector(yy_initWithObjects:count:));
    });
}

- (void)yy_removeObjectAtIndex:(NSUInteger)index{
    @try {
        [self yy_removeObjectAtIndex:index];
    }@catch (NSException *exception) {
        NSString *string = @" LYY crash:";
        if (self.count <= 0) {
            string = [string stringByAppendingString:@"数组个数为零"];
        }else if (self.count <= index) {
            string = [string stringByAppendingString:@"数组移出索引越界"];
        }
        [YYCrashManager yy_crashDealWithException:exception CrashTitle:string];
    }@finally {
        
    }
}

- (void)yy_insertObject:(id)anObject atIndex:(NSUInteger)index{
    @try {
        [self yy_insertObject:anObject atIndex:index];
    }@catch (NSException *exception) {
        NSString *string = @" LYY crash:";
        if (anObject == nil) {
            string = [string stringByAppendingString:@"数组插入数据为空"];
        }else {
            if (self.count <= 0) {
                string = [string stringByAppendingString:@"数组个数为零"];
            }else if (self.count <= index) {
                string = [string stringByAppendingString:@"数组插入索引越界"];
            }
        }
        [YYCrashManager yy_crashDealWithException:exception CrashTitle:string];
    }@finally {
        
    }
}

- (void)yy_setObject:(id)anObject atIndexedSubscript:(NSUInteger)index{
    @try {
        [self yy_setObject:anObject atIndexedSubscript:index];
    }@catch (NSException *exception) {
        NSString *string = @" LYY crash:";
        if (anObject == nil) {
            string = [string stringByAppendingString:@"数组更改数据为空"];
        }else{
            if (self.count <= 0) {
                string = [string stringByAppendingString:@"数组个数为零"];
            }else if (self.count <= index) {
                string = [string stringByAppendingString:@"数组更改索引越界"];
            }
        }
        [YYCrashManager yy_crashDealWithException:exception CrashTitle:string];
    }@finally {
        
    }
}

- (instancetype)yy_objectAtIndex:(NSUInteger)index{
    NSMutableArray *temp = nil;
    @try {
        temp = [self yy_objectAtIndex:index];
    }@catch (NSException *exception) {
        NSString *string = @" LYY crash:";
        if (self.count == 0) {
            string = [string stringByAppendingString:@"数组个数为零"];
        }else if (self.count <= index) {
            string = [string stringByAppendingString:@"数组索引越界"];
        }
        [YYCrashManager yy_crashDealWithException:exception CrashTitle:string];
    }@finally {
        return temp;
    }
}

- (instancetype)yy_objectAtIndexedSubscript:(NSUInteger)index{
    NSMutableArray *temp = nil;
    @try {
        temp = [self yy_objectAtIndexedSubscript:index];
    }@catch (NSException *exception) {
        NSString *string = @" LYY crash:";
        if (self.count == 0) {
            string = [string stringByAppendingString:@"数组个数为零"];
        }else if (self.count <= index) {
            string = [string stringByAppendingString:@"数组索引越界"];
        }
        [YYCrashManager yy_crashDealWithException:exception CrashTitle:string];
    }@finally {
        return temp;
    }
}

- (instancetype)yy_initWithObjects:(const id _Nonnull __unsafe_unretained *)objects count:(NSUInteger)cnt{
    id instance = nil;
    @try {
        instance = [self yy_initWithObjects:objects count:cnt];
    }@catch (NSException *exception) {
        NSString *string = @" LYY crash:添加的数据中有空对象";
        [YYCrashManager yy_crashDealWithException:exception CrashTitle:string];
        NSInteger newIndex = 0;
        id _Nonnull __unsafe_unretained newObjects[cnt];
        for (int i = 0; i < cnt; i++) {
            if (objects[i] != nil) {
                newObjects[newIndex] = objects[I];
                newIndex++;
            }
        }
        instance = [self yy_initWithObjects:newObjects count:newIndex];
    }@finally {
        return instance;
    }
}

第三类: KVO的Crash与防护

KVO(Key-Value-Observing)键值观察,其技术原理就是通过isa swizzle技术添加被观察对象中间类,并重写相应的方法来监听键值变化。当被观察对象属性被修改后,则对象就会接收到通知,即每次指定的被观察对象的属性被修改后,KVO就会自动通知相应的观察者。

KVO引起Crash的场景:

  • observer已销毁,但是未及时移除监听,引起EXC_BAD_ACCESS崩溃;
  • addObserver与removeObserver不匹配;
    1.移除了未注册的观察者,导致崩溃。
    2.重复移除多次,移除次数多于添加次数,导致崩溃。
    3.重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
  • 添加了观察者,但未实现observeValueForKeyPath:ofObject:change:context: 方法,导致崩溃。
  • 添加或者移除时 keypath == nil,导致崩溃。

KVO的防护原理分析:

  • 通过Method Swizzle拦截系统关于KVO的相关方法,替换成自己自定义的方法,包括添加/移除监听和被观察者销毁的dealloc;
  • 在观察者和被观察者之间建立一个YYKVODelegate 对象,两者之间通过 YYKVODelegate对象建立联系。然后在添加和移除操作时,将 KVO 的相关信息例如 observer、keyPath、options、context 保存为 YYKVOInfo 对象,并添加到 KVODelegate对象维护的关系哈希表中;
  • 在添加和移除观察者时,调用系统的方法传入YYKVODelegate对象,YYKVODelegate才是真正的观察者对象,KVO的监听回调都是由YYKVODelegate来处理的;
  • 为了避免被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO 导致崩溃。利用 Method Swizzling 实现了自定义的dealloc,在系统dealloc调用之前,将多余的观察者移除掉。

相关代码如下所示:

  • YYKVODelegate对象
- (BOOL)addKVOInfoToMapsWithObserver:(NSObject *)observer
                          forKeyPath:(NSString *)keyPath
                             options:(NSKeyValueObservingOptions)options
                             context:(void *)context{
    BOOL success;
    //先判断有没有重复添加,有的话报错;没有的话,添加到数组中
    [_bmp_kvoLock lock];
    NSMutableArray <YYKVOInfo *> *kvoInfos = [self getKVOInfosForKeypath:keyPath];
    __block BOOL isExist = NO;
    [kvoInfos enumerateObjectsUsingBlock:^(YYKVOInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj->_observer == observer) {
            isExist = YES;
        }
    }];
    if (isExist) {//已经存在了
        success = NO;
    }else{
        //将监听元素封装在YYKVOInfo模型中
        YYKVOInfo *info = [[YYKVOInfo alloc]init];
        info->_observer = observer;
        info->_md5Str = YY_md5StringOfObject(observer);
        info->_keyPath = keyPath;
        info->_options = options;
        info->_context = context;
        [kvoInfos addObject:info];
        //最后添加到哈希表中保存
        [self setKVOInfos:kvoInfos ForKeypath:keyPath];
        success = YES;
    }
    [_bmp_kvoLock unlock];
    return success;
}
  • NSObject分类:
+ (void)yy_openKVOExchangeMethod{
    ///添加观察者
    YY_EXChangeInstanceMethod([NSObject class], @selector(addObserver:forKeyPath:options:context:), [NSObject class], @selector(yy_addObserver:forKeyPath:options:context:));
    ///移除观察者-01
    YY_EXChangeInstanceMethod([NSObject class], @selector(removeObserver:forKeyPath:), [NSObject class], @selector(yy_removeObserver:forKeyPath:));
    ///移除观察者-02
    YY_EXChangeInstanceMethod([NSObject class], @selector(removeObserver:forKeyPath:context:), [NSObject class], @selector(yy_removeObserver:forKeyPath:context:));
    ///观察者销毁
    YY_EXChangeInstanceMethod([NSObject class], NSSelectorFromString(@"dealloc"), [NSObject class], @selector(YYKVO_dealloc));
}

- (void)yy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
    if ([YYCrashProtectorManager isIgnoreKVOProtectionForClass:[self class]]) {
        __weak typeof(self) weakSelf = self;
        objc_setAssociatedObject(self, KVOProtectorKey, KVOProtectorValue, OBJC_ASSOCIATION_RETAIN);
        ///将观察者添加到yyKVODelegate维护的哈希表中去,可避免重复添加
        [self.yyKVODelegate addKVOInfoToMapsWithObserver:observer forKeyPath:keyPath options:options context:context success:^{
            ///调用系统方法:真正的将观察者设置为: yyKVODelegate代理对象
            [weakSelf yy_addObserver:weakSelf.yyKVODelegate forKeyPath:keyPath options:options context:context];
        } failure:^(NSError *error) {
            NSLog(@" YY -- 监听重复添加");
        }];
    }else{
        [self yy_addObserver:observer forKeyPath:keyPath options:options context:context];
    }
}

- (void)yy_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    if ([YYCrashProtectorManager isIgnoreKVOProtectionForClass:[self class]]) {
        ///从yyKVODelegate维护的哈希表中移除观察者,可避免重复移除
        if ([self.yyKVODelegate removeKVOInfoInMapsWithObserver:observer forKeyPath:keyPath]) {
            ///调用系统方法:真正的移除观察者
            [self yy_removeObserver:self.yyKVODelegate forKeyPath:keyPath];
        }else{
            NSLog(@"重复移除观察者 观察者与被观察者不匹配");
        }
    }else{
        [self yy_removeObserver:observer forKeyPath:keyPath];
    }
}

- (void)YYKVO_dealloc{
    if ([YYCrashProtectorManager isIgnoreKVOProtectionForClass:[self class]]) {
        NSString *value = (NSString *)objc_getAssociatedObject(self, KVOProtectorKey);
        if ([value isEqualToString:KVOProtectorValue]) {
            NSArray *keypaths = [self.yyKVODelegate getAllKeypaths];
            ///被观察者对象已经被销毁 但监听尚未全部移除
            if (keypaths.count > 0) {
                NSLog(@"有多余的监听 尚未全部移除");
            }
            [keypaths enumerateObjectsUsingBlock:^(NSString *keyPath, NSUInteger idx, BOOL * _Nonnull stop) {
                ///调用系统的方法:将尚未移除的监听全部移除
                [self yy_removeObserver:self.yyKVODelegate forKeyPath:keyPath];
            }];
        }
    }
    [self YYKVO_dealloc];
}

第四类: KVC的Crash与防护

KVC在使用的过程中造成crash的场景有:

  • 非对象属性,设置value值为空,造成崩溃;
  • key为nil,造成崩溃;
  • key不是对象的属性,造成崩溃;
  • forKeyPath设置不正确,造成崩溃.

KVC造成崩溃的代码实例:

- (void)function_01{
    //非对象属性,设置value值为空
    //reason:'[<YYPerson 0x600000a117c0> setNilValueForKey]: could not set nil as the value for the key age.'
    YYPerson *p = [[YYPerson alloc]init];
    [p setValue:nil forKey:@"age"];
}

- (void)function_02{
    //key为nil,造成崩溃
    //reason:'-[YYPerson setValue:forKey:]:attempt to set a value for a nil key'
    YYPerson *p = [[YYPerson alloc]init];
    [p setValue:@30 forKey:nil];
}

- (void)function_03{
    //key不是对象的属性
    //reason:'[<YYPerson 0x6000020c9740> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key height.'
    YYPerson *p = [[YYPerson alloc]init];
    [p setValue:@30 forKey:@"height"];
}

- (void)function_04{
    //forKeyPath不正确
    //reason:'[<YYPerson 0x6000032e0700> valueForUndefinedKey:]: this class is not key value coding-compliant for the key address.'
    YYPerson *p = [[YYPerson alloc]init];
    [p setValue:@"123" forKeyPath:@"address.street"];
}

KVC的具体防护
根据KVC底层的实现原理我们知道:

  • 当setValue:forKey调用失败,如果我们没有手动去实现- (void)setValue:(id)value forUndefinedKey:(NSString *)key这个方法,就会抛出异常造成崩溃;
  • 当valueForKey:调用失败,如果我们没有手动去实现- (id)valueForUndefinedKey:(NSString *)key这个方法就会抛出异常造成崩溃;
  • 当setValue:forKey针对非对象属性传值为空时,如果没有手动去实现-(void)setNilValueForKey:(NSString *)key这个方法,就会抛出异常造成崩溃;
    上面三种情况只要手动实现了对应的方法就不会出现crash,就可以解决key不是对象的属性,造成崩溃,forKeyPath设置不正确,造成崩溃.非对象属性,设置value值为空,造成崩溃;
  • 针对key为nil,造成崩溃;需要利用 Method Swizzling 方法拦截系统的setValue:forKey方法,替换成自定义的方法并实现.

代码实现: NSObject分类:

@implementation NSObject (YYKVCException)

+ (void)yy_openKVCExchangeMethod{
    YY_EXChangeInstanceMethod([NSObject class], @selector(setValue:forKey:), [self class], @selector(yy_setValue:forKey:));
}

- (void)yy_setValue:(id)value forKey:(NSString *)key{
    if (key == nil) {
        NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setNilValueForKey]: could not set nil as the value for the key %@.",NSStringFromClass([self class]),self,key];
        NSLog(@"%@", crashMessages);
        return;
    }
    [self yy_setValue:value forKey:key];
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> forUndefinedKey]: could not set nil as the value for the key %@.",NSStringFromClass([self class]),self,key];
    NSLog(@"%@", crashMessages);
}

- (id)valueForUndefinedKey:(NSString *)key{
    NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> valueForUndefinedKey]: could not set nil as the value for the key %@.",NSStringFromClass([self class]),self,key];
    NSLog(@"%@", crashMessages);
    return self;
}

- (void)setNilValueForKey:(NSString *)key{
    NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setNilValueForKey]: could not set nil as the value for the key %@.",NSStringFromClass([self class]),self,key];
    NSLog(@"%@", crashMessages);
}

@end

第五类: NSTimer的防护主要是为了解决循环引用所造成的内存泄漏

NSTimer是iOS开发中一种比较常见的定时器;是基于RunLoop(运行循环)实现的,那么NSTimer必须加入到RunLoop中才能正常的工作.

NSTimer定时器会出现不可避免的误差,所以其精确性不是很高;产生误差的原因如下:

与NSRunLoop的运行机制有关,因为RunLoop每跑完一次圈再去检查当前累计时间是否已经达到定时器设置的间隔时间,如果未达到,RunLoop将进入下一轮任务,待任务结束之后再去检查当前累计时间,而此时的累计时间可能已经超过了定时器的间隔时间,故会存在误差。

NSTimer定时器为什么容易引发内存泄漏:

因为NSTimer必须要加入RunLoop中才能开始工作,则RunLoop强引用NSTimer,而实例化创建NSTimer会对Target强引用;RunLoop始终存在导致NSTimer无法释放则会导致Target无法释放,所以会引起内存泄漏.

Snip20210129_82.png

NSTimer内存泄漏的防护:

  • 利用RunTime的method swizzle拦截系统的创建NSTimer的方法,替换成自己定义的方法;
  • 引入一个中间类YYTimerSubTarget,让对象成为定时器的监听者,且其自己实现定时器的回调方法fireProxyTimer: 且YYTimerSubTarget对象接收了与定时器相关所有属性timeInterval/原始target/原始selector/userInfo/repeats;
  • 在YYTimerSubTarget对象的定时器回调fireProxyTimer:中,去调用原始target的原始selector方法,且加入对原始target存在性的判断,当原始target存在时执行定时回调方法,当原始target不存在时,释放定时器,然后释放YYTimerSubTarget对象;这样就不会有内存泄漏了.
Snip20210129_84.png

代码实现:
中间类: YYTimerSubTarget

@interface YYTimerSubTarget : NSObject

/**
 YYTimerSubTarget 初始化方法
 @param ti ti
 @param aTarget aTarget
 @param aSelector aSelector
 @param userInfo userInfo
 @param yesOrNo yesOrNo
 @param errorHandler 错误回调
 @return subTarget实例
 */
+ (instancetype)targetWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo catchErrorHandler:(void(^)(YYCrashError * error))errorHandler;

@end
@interface YYTimerSubTarget ()
{
    NSTimeInterval _ti;
    __weak id _aTarget;
    SEL _aSelector;
    __weak id _userInfo;
    BOOL _yesOrNo;
    NSString *_targetClassName;
    YYErrorHandler _errorHandler;
}
@end

@implementation YYTimerSubTarget

+ (instancetype)targetWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo catchErrorHandler:(void (^)(YYCrashError *))errorHandler{
    return [[self alloc]initWithTimeInterval:ti target:aTarget selector:aSelector userInfo:userInfo repeats:yesOrNo catchErrorHandler:errorHandler];
}

- (instancetype)initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo catchErrorHandler:(void (^)(YYCrashError *))errorHandler{
    self = [super init];
    if (self) {
        _ti = ti;
        _aTarget = aTarget;
        _aSelector = aSelector;
        _userInfo = userInfo;
        _yesOrNo = yesOrNo;
        _errorHandler = errorHandler;
    }
    return self;
}

- (void)fireProxyTimer:(NSTimer *)timer{
    if (_aTarget) {
        if ([_aTarget respondsToSelector:_aSelector]) {
            YY_SuppressPerformSelectorLeakWarning(
                [_aTarget performSelector:_aSelector withObject:timer];
            );
        }
    }else{
        //_aTarget已经被销毁
        if (_errorHandler) {
            _errorHandler(nil);
        }
        //销毁定时器 -- 销毁中间对象
        [timer invalidate];
        timer = nil;
    }
}

@end

NSTimer分类

@implementation NSTimer (YYException)

+ (void)yy_openCrashExchangeMethod{
    YY_EXChangeClassMethod([NSTimer class], @selector(timerWithTimeInterval:target:selector:userInfo:repeats:), @selector(yy_timerWithTimeInterval:target:selector:userInfo:repeats:));
    
    YY_EXChangeClassMethod([NSTimer class], @selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:), @selector(yy_scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:));
}

+ (NSTimer *)yy_timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats{
    if (repeats == NO) {
        return [self yy_timerWithTimeInterval:timeInterval target:target selector:selector userInfo:userInfo repeats:repeats];
    }else{
        ///过滤掉系统类和黑名单中的类
        if (![YYCrashProtectorManager isIgnoreExeptionProtectionForClass:[target class]]) {
            YYTimerSubTarget *subTarget = [YYTimerSubTarget targetWithTimeInterval:timeInterval target:target selector:selector userInfo:userInfo repeats:repeats catchErrorHandler:^(YYCrashError *error) {
                
            }];
            ///调用系统的创建定时器的方法 target参数传入的是中间对象
            return [self yy_timerWithTimeInterval:timeInterval target:subTarget selector:NSSelectorFromString(@"fireProxyTimer:") userInfo:userInfo repeats:repeats];
        }else{
            return [self yy_timerWithTimeInterval:timeInterval target:target selector:selector userInfo:userInfo repeats:repeats];
        }
    }
}

+ (NSTimer *)yy_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats{
    if (repeats == NO) {
        return [self yy_scheduledTimerWithTimeInterval:timeInterval target:target selector:selector userInfo:userInfo repeats:repeats];
    }else{
        ///过滤掉系统类和黑名单中的类
        if (![YYCrashProtectorManager isIgnoreExeptionProtectionForClass:[target class]]) {
            YYTimerSubTarget *subTarget = [YYTimerSubTarget targetWithTimeInterval:timeInterval target:target selector:selector userInfo:userInfo repeats:repeats catchErrorHandler:^(YYCrashError *error) {
                
            }];
            ///调用系统的创建定时器的方法 target参数传入的是中间对象
            return [self yy_scheduledTimerWithTimeInterval:timeInterval target:subTarget selector:NSSelectorFromString(@"fireProxyTimer:") userInfo:userInfo repeats:repeats];
        }else{
            return [self yy_scheduledTimerWithTimeInterval:timeInterval target:target selector:selector userInfo:userInfo repeats:repeats];
        }
    }
}

@end

待续......

第五类: Bad Access 野指针 访问已经被系统回收的内存

https://juejin.cn/post/6874435201632583694#heading-10

相关文章

网友评论

      本文标题:iOS的Exception类型与防护

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