KVO(Key-Value-Observing)键值观察,其技术原理就是通过isa swizzle技术添加被观察对象中间类,并重写相应的方法来监听键值变化。当被观察对象属性被修改后,则对象就会接收到通知,即每次指定的被观察对象的属性被修改后,KVO就会自动通知相应的观察者。
KVO引起Crash异常的场景
第一种:observer已销毁,但是未及时移除监听,引起EXC_BAD_ACCESS崩溃
模拟场景:
- 创建两个控制器
ViewController
与YYNextVC
,在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 的相关信息例如
observer
、keyPath
、options
、context
保存为 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监听通常都是我们自定义的类,所以做了系统类的过滤。