iOS异常系列01 -- KVO的异常与防护

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

KVO引起Crash异常的场景

第一种:observer已销毁,但是未及时移除监听,引起EXC_BAD_ACCESS崩溃

模拟场景:

  • 创建两个控制器ViewControllerYYNextVC,在ViewController初始化一个YYPerson模型实例对象,并且传递给YYNextVC,在YYNextVC中进行name属性KVO监听;
  • 在YYNextVC出栈销毁时,YYNextVC并没有移除对YYPerson的监听,当在ViewController中改变YYPerson模型实例对象的属性时,引起崩溃,因为YYNextVC监听者已经被销毁了。

核心代码如下:

Snip20210315_136.png
Snip20210315_137.png
第二种:addObserver与removeObserver不匹配
  • 移除了未注册的观察者,导致崩溃。
  • 重复移除多次,移除次数多于添加次数,导致崩溃。
  • 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
第三种:添加了观察者,但未实现observeValueForKeyPath:ofObject:change:context: 方法,导致崩溃。
第四种:添加或者移除时keypath == nil,导致崩溃

针对上面的crash异常场景,需要做KVO的crash异常防护。

KVO的crash异常防护原理分析

  • 通过Method Swizzle拦截系统关于KVO的相关方法,替换成自己自定义的方法,包括添加/移除KVO监听以及被观察者销毁的dealloc;
  • 在观察者和被观察者之间建立一个YYKVODelegate 对象,两者之间通过YYKVODelegate对象建立联系。然后在添加和移除操作时,将 KVO 的相关信息例如 observerkeyPathoptionscontext保存为 YYKVOInfo对象,并添加到 KVODelegate对象维护的关系哈希表中;
  • 在添加和移除观察者时,调用系统的方法传入YYKVODelegate对象,YYKVODelegate才是真正的观察者对象,KVO的监听回调都是由YYKVODelegate来处理的;
  • 在执行KVO观察者回调函数时,都会检测观察者是否存在,存在才会执行监听回调,避免观察者已经销毁还向其发送消息,导致崩溃。
  • 被观察者在dealloc时仍然注册着观察者得KVO监听,可利用 Method Swizzling实现了自定义的dealloc,在系统dealloc调用之前,将多余的观察者全部移除掉。

具体实现代码如下:

  • 给NSObject添加一个分类NSObject (YYKVOProtector),主要是实现Method Swizzle;可以在AppDelegate.m文件中开启KVO防护即调用[NSObject yy_openKVOExchangeMethod]
//  KVO crash异常防护

#import 

@interface NSObject (YYKVOProtector)

//开启KVO异常防护
+ (void)yy_openKVOExchangeMethod;

@end
#import "NSObject+YYKVOProtector.h"
#import "YYRuntimeHelper.h"
#import 
#import "YYKVODelegate.h"

//声明保存需要忽略的类前缀数组
static NSArray *_ignorePrefixes;

static void *YYKVOProtectorKey = &YYKVOProtectorKey;
static NSString *const YYKVOProtectorValue = @"YY_KVOProtector";
static void *YYKVODelegateKey = &YYKVODelegateKey;

/* 是否是系统类 */
static inline BOOL IsSystemClass(Class cls){
    __block BOOL isSystem = NO;
    NSString *className = NSStringFromClass(cls);
    if ([className hasPrefix:@"NS"]) {
        isSystem = YES;
        return isSystem;
    }
    NSBundle *mainBundle = [NSBundle bundleForClass:cls];
    if (mainBundle == [NSBundle mainBundle]) {
        isSystem = NO;
    }else{
        isSystem = YES;
    }
    if (_ignorePrefixes.count>0) {
        [_ignorePrefixes enumerateObjectsUsingBlock:^(NSString * prefix, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([className hasPrefix:prefix]) {
                isSystem = YES;
                *stop = YES;
            }
        }];
    }
    return isSystem;
}


@implementation NSObject (YYKVOProtector)

+ (void)yy_openKVOExchangeMethod{
    //方法交换 替换系统的添加/移除KVO观察者方法
    [YYRuntimeHelper yy_instanceMethodSwizzlingithClass:[NSObject class] originalSEL:@selector(addObserver:forKeyPath:options:context:) swizzledSEL:@selector(yy_addObserver:forKeyPath:options:context:)];
    
    [YYRuntimeHelper yy_instanceMethodSwizzlingithClass:[NSObject class] originalSEL:@selector(removeObserver:forKeyPath:context:) swizzledSEL:@selector(yy_removeObserver:forKeyPath:context:)];
    
    [YYRuntimeHelper yy_instanceMethodSwizzlingithClass:[NSObject class] originalSEL:@selector(removeObserver:forKeyPath:) swizzledSEL:@selector(yy_removeObserver:forKeyPath:)];
    
    //方法交换 替换被观察者的dealloc方法
    [YYRuntimeHelper yy_instanceMethodSwizzlingithClass:[NSObject class] originalSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(yy_KVO_dealloc)];
}

- (void)setKVODelegate:(YYKVODelegate *)kVODelegate{
    objc_setAssociatedObject(self,YYKVODelegateKey,kVODelegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (YYKVODelegate *)kVODelegate{
    id yyKVODelegate = objc_getAssociatedObject(self,YYKVODelegateKey);
    if (yyKVODelegate == nil) {
        yyKVODelegate = [[YYKVODelegate alloc]init];
        self.kVODelegate = yyKVODelegate;
    }
    return yyKVODelegate;
}

- (void)yy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
    if (keyPath == nil) {
        return;
    }
    //过滤掉系统类,主要针对自定义类
    if (!IsSystemClass(self.class)) {
        __weak typeof(self) weakSelf = self;
        //给被KVO监听对象加上标记,然后dealloc方法中针对被KVO监听对象进行一些操作
        objc_setAssociatedObject(self, YYKVOProtectorKey,YYKVOProtectorValue, OBJC_ASSOCIATION_RETAIN);
        //将KVO监听的相关元素封装成KVOInfo模型,并保存在kVODelegate对象的Map中
        [self.kVODelegate addKVOInfoToMapsWithObserver:observer forKeyPath:keyPath options:options context:context success:^{
            //本质是调用系统设置KVO监听函数,KVO的监听者为kVODelegate
            //所以KVO的监听回调在kVODelegate对象类中
            [self yy_addObserver:weakSelf.kVODelegate forKeyPath:keyPath options:options context:context];
        } failure:^(NSError *error) {
            NSLog(@" error = %@",error);
        }];
    }else{
        [self yy_addObserver:self.kVODelegate forKeyPath:keyPath options:options context:context];
    }
}

- (void)yy_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    if (!IsSystemClass(self.class)) {
        if ([self.kVODelegate removeKVOInfoInMapsWithObserver:observer forKeyPath:keyPath]) {
            [self yy_removeObserver:observer forKeyPath:keyPath];
        }else{
            NSLog(@" error >> %@不存在keyPath = %@的KVO监听",NSStringFromClass(self.class),keyPath);
        }
    }else{
        [self yy_removeObserver:observer forKeyPath:keyPath];
    }
}

- (void)yy_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context{
    if (!IsSystemClass(self.class)) {
        if ([self.kVODelegate removeKVOInfoInMapsWithObserver:observer forKeyPath:keyPath]) {
            [self yy_removeObserver:observer forKeyPath:keyPath context:context];
        }else{
            NSLog(@" error >> %@不存在keyPath = %@的KVO监听",NSStringFromClass(self.class),keyPath);
        }
    }else{
        [self yy_removeObserver:observer forKeyPath:keyPath context:context];
    }
}

- (void)yy_KVO_dealloc{
    if (!IsSystemClass(self.class)) {
        NSString *value = (NSString *)objc_getAssociatedObject(self, YYKVOProtectorKey);
        if ([value isEqualToString:YYKVOProtectorValue]) {
            NSArray *keypaths = [self.kVODelegate getAllKeypaths];
            if (keypaths.count > 0) {
                [keypaths enumerateObjectsUsingBlock:^(NSString *keyPath, NSUInteger idx, BOOL * _Nonnull stop) {
                    //错误信息
                    [self yy_removeObserver:self.kVODelegate forKeyPath:keyPath];
                }];
            }
        }
    }
    [self yy_KVO_dealloc];
}

@end
  • KVO实际观察者类YYKVODelegate,保存KVO信息的模型KVOInfo
#import 

@interface YYKVODelegate : NSObject

/**
 将添加kvo时的相关信息加入到关系maps中,对应原有的添加观察者
 带成功和失败的回调
 @param observer observer观察者
 @param keyPath keyPath
 @param options options
 @param context context
 @param success success 成功的回调
 @param failure failure 失败的回调
 */
- (void)addKVOInfoToMapsWithObserver:(NSObject *)observer
                      forKeyPath:(NSString *)keyPath
                         options:(NSKeyValueObservingOptions)options
                         context:(void *)context
                         success:(void(^)(void))success
                         failure:(void(^)(NSError *error))failure;

/**
 将添加kvo时的相关信息加入到关系maps中,对应原有的添加观察者不带成功和失败的回调
 @param observer 实际观察者
 @param keyPath keyPath
 @param options options
 @param context context
 @return return 是否添加成功
 */
- (BOOL)addKVOInfoToMapsWithObserver:(NSObject *)observer
                          forKeyPath:(NSString *)keyPath
                             options:(NSKeyValueObservingOptions)options
                             context:(void *)context;

/**
 从关系maps中移除观察者 对应原有的移除观察者操作
 @param observer 实际观察者
 @param keyPath keypath
 @return 是否移除成功
 如果重复移除,会返回NO
 */
- (BOOL)removeKVOInfoInMapsWithObserver:(NSObject *)observer
                             forKeyPath:(NSString *)keyPath;

- (NSArray *)getAllKeypaths;

@end
#import "YYKVODelegate.h"
#include 
#include 

static NSLock *_bmp_kvoLock;

static inline NSString *BMP_md5StringOfObject(NSObject *object){
    NSString *string = [NSString stringWithFormat:@"%p",object];
    const char *str = string.UTF8String;
    uint8_t buffer[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), buffer);
    NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
    for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
        [output appendFormat:@"%02x", buffer[i]];
    }
    return output;
}

//存储KVO监听相关元素的模型
@interface KVOInfo: NSObject

@end

@implementation KVOInfo
{
    @package
    void *_context;
    NSKeyValueObservingOptions _options;
    __weak NSObject *_observer;
    NSString *_keyPath;
    NSString *_md5Str;
}
@end

@implementation YYKVODelegate
{
    @private
    //一对多即一个属性对应多个观察者
    NSMutableDictionary *> *_keyPathMaps;
}

- (instancetype)init{
    self = [super init];
    if (nil != self) {
        _keyPathMaps = [NSMutableDictionary dictionary];
        _bmp_kvoLock = [[NSLock alloc]init];
    }
    return self;
}

- (BOOL)addKVOInfoToMapsWithObserver:(NSObject *)observer
                          forKeyPath:(NSString *)keyPath
                             options:(NSKeyValueObservingOptions)options
                             context:(void *)context{
    BOOL success;
    //先判断有没有重复添加,有的话报错,没有的话,添加到数组中
    [_bmp_kvoLock lock];
    NSMutableArray  *kvoInfos = [self getKVOInfosForKeypath:keyPath];
    __block BOOL isExist = NO;
    [kvoInfos enumerateObjectsUsingBlock:^(KVOInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj->_observer == observer) {
            isExist = YES;
        }
    }];
    //已经存在了
    if (isExist) {
        success = NO;
    }else{//不存在 创建模型 加入map中
        KVOInfo *info = [[KVOInfo alloc]init];
        info->_observer = observer;
        info->_md5Str = BMP_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;
}

- (void)addKVOInfoToMapsWithObserver:(NSObject *)observer
                          forKeyPath:(NSString *)keyPath
                             options:(NSKeyValueObservingOptions)options
                             context:(void *)context
                             success:(void(^)(void))success
                             failure:(void(^)(NSError *error))failure{
    [_bmp_kvoLock lock];
    //先判断有没有重复添加,有的话报错,没有的话,添加到数组中
    NSMutableArray  *kvoInfos = [self getKVOInfosForKeypath:keyPath];
    __block BOOL isExist = NO;
    [kvoInfos enumerateObjectsUsingBlock:^(KVOInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj->_observer == observer) {
            isExist = YES;
        }
    }];
    //已经存在了
    if (isExist) {
        if (failure) {
            NSInteger code = -1234;
            NSString *msg = [NSString stringWithFormat:@"\n observer重复添加:\n observer:%@\n keypath:%@ \n",observer,keyPath];
            NSError *error = [NSError errorWithDomain:@"com.YYKVODelegate" code:code userInfo:@{@"NSLocalizedDescriptionKey":msg}];
            failure(error);
        }
    }else{
        KVOInfo *info = [[KVOInfo alloc]init];
        info->_observer = observer;
        info->_md5Str = BMP_md5StringOfObject(observer);
        info->_keyPath = keyPath;
        info->_options = options;
        info->_context = context;
        [kvoInfos addObject:info];
        [self setKVOInfos:kvoInfos ForKeypath:keyPath];
        if (success) {
            success();
        }
    }
    [_bmp_kvoLock unlock];
}

- (BOOL)removeKVOInfoInMapsWithObserver:(NSObject *)observer
                             forKeyPath:(NSString *)keyPath{
    [_bmp_kvoLock lock];
    BOOL success;
    NSMutableArray  *kvoInfos = [self getKVOInfosForKeypath:keyPath];
    __block BOOL isExist = NO;
    __block KVOInfo *kvoInfo;
    [kvoInfos enumerateObjectsUsingBlock:^(KVOInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj->_md5Str isEqualToString:BMP_md5StringOfObject(observer)]) {
            isExist = YES;
            kvoInfo = obj;
        }
    }];
    if (kvoInfo) {
        [kvoInfos removeObject:kvoInfo];
        //说明该keypath没有observer观察,可以移除该键
        if (kvoInfos.count == 0) {
            [_keyPathMaps removeObjectForKey:keyPath];
        }
    }
    success = isExist;
    [_bmp_kvoLock unlock];
    return success;
}

#pragma mark 获取keypath对应的所有观察者
- (NSMutableArray *)getKVOInfosForKeypath:(NSString *)keypath{
    if ([_keyPathMaps.allKeys containsObject:keypath]) {
        return [_keyPathMaps objectForKey:keypath];
    }else{
        return [NSMutableArray array];
    }
}

#pragma mark  设置keypath对应的观察者数组
- (void)setKVOInfos:(NSMutableArray *)kvoInfos ForKeypath:(NSString *)keypath{
    if (![_keyPathMaps.allKeys containsObject:keypath]) {
        if (keypath) {
            _keyPathMaps[keypath] = kvoInfos;
        }
    }
}

#pragma mark 实际观察者执行相对应的监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSMutableArray  *kvoInfos = [self getKVOInfosForKeypath:keyPath];
    [kvoInfos enumerateObjectsUsingBlock:^(KVOInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj->_keyPath isEqualToString:keyPath]) {
            NSObject *observer = obj->_observer;
            //当观察者存在时,才会调用系统的KVO回调
            if (observer) {
                [observer observeValueForKeyPath:keyPath ofObject:object change:change context:context];
            }
        }
    }];
}

#pragma mark 获取所有被观察的keypaths
- (NSArray *)getAllKeypaths{
    NSArray *keyPaths = _keyPathMaps.allKeys;
    return keyPaths;
}
@end
  • 方法交换的工具类
#import 

@interface YYRuntimeHelper : NSObject

//实例方法交换
+ (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL;

//类方法交换
+ (void)yy_classMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL;

@end
#import "YYRuntimeHelper.h"
#import 

@implementation YYRuntimeHelper

+ (void)yy_instanceMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) {
        return;
    }
    
    Method original_method = class_getInstanceMethod(cls,originalSEL);
    Method swizzled_method = class_getInstanceMethod(cls,swizzledSEL);
    
    IMP original_imp = method_getImplementation(original_method);
    IMP swizzled_imp = method_getImplementation(swizzled_method);
    
    if (!original_method) {
        //在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
        class_addMethod(cls,originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
        method_setImplementation(swizzled_method,
                                 imp_implementationWithBlock(^(id self,SEL _cmd){
            NSLog(@"来了一个空的 imp");
        }));
    }

    BOOL didAddMethod = class_addMethod(cls,originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
    if (didAddMethod) {
        class_replaceMethod(cls,swizzledSEL,original_imp, method_getTypeEncoding(original_method));
    }else{
        method_exchangeImplementations(original_method,swizzled_method);
    }
}

+ (void)yy_classMethodSwizzlingithClass:(Class)cls originalSEL:(SEL)originalSEL swizzledSEL:(SEL)swizzledSEL{
    if (!cls) {
        return;
    }
    
    class_getClassMethod(cls,originalSEL);
    Method original_method = class_getClassMethod([cls class],originalSEL);
    Method swizzled_method = class_getClassMethod([cls class],swizzledSEL);
    
    IMP original_imp = method_getImplementation(original_method);
    IMP swizzled_imp = method_getImplementation(swizzled_method);
    
    if (!original_method) {
        //在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
        //object_getClass(cls) 获取元类
        class_addMethod(object_getClass(cls),originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
        method_setImplementation(swizzled_method,
                                 imp_implementationWithBlock(^(id self,SEL _cmd){
            NSLog(@"来了一个空的 imp");
        }));
    }

    BOOL didAddMethod = class_addMethod(object_getClass(cls),originalSEL,swizzled_imp, method_getTypeEncoding(swizzled_method));
    if (didAddMethod) {
        class_replaceMethod(object_getClass(cls),swizzledSEL,original_imp, method_getTypeEncoding(original_method));
    }else{
        method_exchangeImplementations(original_method,swizzled_method);
    }
}
@end
  • 由于是给NSObject添加的分类,而添加KVO监听通常都是我们自定义的类,所以做了系统类的过滤。

你可能感兴趣的:(iOS异常系列01 -- KVO的异常与防护)