小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
自定义KVO的多元素观察和移除观察
多元素观察
在上文我们已经实现了针对属nickName
的监听,那么如果我们需要监听多个属性
呢?比如我们要同时监听Person
的两个属性nickName
和realName
;
@interface Person : NSObject
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, copy) NSString *realName;
@end
复制代码
那么此时显然我们之前保存观察者的方式已经不合适了;
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCKKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
复制代码
这个时候我们就需要将观察者收集起来,比如我们新建一个类CKKVOInfo
:
typedef NS_OPTIONS(NSUInteger, CKKeyValueObservingOptions) {
CKKeyValueObservingOptionNew = 0x01,
CKKeyValueObservingOptionOld = 0x02,
};
@interface CKKVOInfo : NSObject
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, weak) NSObject *observer; // 需要使用weak修饰,否则会发生循环引用
@property (nonatomic, assign) CKKeyValueObservingOptions options;
/// 初始化方法
/// @param observer observer description
/// @param keyPath keyPath description
/// @param options options description
- (instancetype)initWithObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(CKKeyValueObservingOptions)options;
@end
@implementation CKKVOInfo
- (instancetype)initWithObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(CKKeyValueObservingOptions)options {
self = [super init];
if (self) {
self.observer = observer;
self.keyPath = keyPath;
self.options = options;
}
return self;
}
@end
复制代码
我们使用CKKVOInfo
这个类来保存keyPath
和observer
等一系列数据;那么在外界我们就需要一个CKKVOInfo
的数组集合来收集信息;
代码修改如下:
需要注意的是此处代码:
// 给观察者发送消息
SEL observerSEL = @selector(ck_observeValueForKeyPath:ofObject:change:context:);
((void (*) (id, SEL, NSString *, id, NSMutableDictionary *, void *)) (void *)objc_msgSend)(info.observer, observerSEL, keyPath, self, change, NULL);
复制代码
以前可以通过修改Xcode
设置项 代码可以写为:
// 给观察者发送消息
SEL observerSEL = @selector(ck_observeValueForKeyPath:ofObject:change:context:);
objc_msgSend(info.observer, observerSEL, keyPath, self, change, NULL);
复制代码
但是这种写法,在Xcode13
中将会报错:
Too many arguments to function call, expected 0, have 6
苹果并不希望开发者过多的使用底层API;
移除观察者
- 每一次移除一个
keyPath
,我们就需要从mArray
中移除一个CKKVOInfo
; - 每一次从
mArray
中移除CKKVOInfo
之后,我们都需要重新设置mArray
; object_setClass
重新修改isa
,将其指向原来的父类
;
自定义KVO的自动销毁
在之前自定义的KVO
中,我们发现销毁的时候我们需要在dealloc
中手动去调用ck_removeObserver
方法,才能达到销毁KVO
的目的
- (void)dealloc {
[self.person ck_removeObserver:self forKeyPath:@"nickName"];
[self.person ck_removeObserver:self forKeyPath:@"realName"];
NSLog(@"%s", __func__);
}
复制代码
那么有没有更简单的方法呢,不用手动调用,让其主动调用自动销毁呢?
这个时候我们可能就会想到,监听dealloc
方法;通过方法交换,在我们交换过的dealloc
方法中去释放监听,移除KVO
;
Method oriMethod = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
Method swiMethod = class_getInstanceMethod([self class], @selector(myDealloc));
method_exchangeImplementations(oriMethod, swiMethod);
复制代码
那么是不是这样就可以了呢?需要注意的是,此时我们获取dealloc
方法的时候,如果本类没有dealloc
方法,那么就回去寻找父类
,这样就有可能造成不必要的问题,这其实也是runtime
的一个坑点,我们需要注意!我们应该只针对本类进行处理,而不应该影响到其他类;
我们可以在第一次动态生成子类的时候,给类添加一个dealloc
方法:
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)ck_dealloc, deallocTypes);
复制代码
我们把
dealloc
方法写在动态子类中,即使本类实现了dealloc
在里边错了其他操作,我们也不会影响到原来的逻辑;
给ck_dealloc
一个实现:
static void ck_dealloc(id self,SEL _cmd){
Class superClass = [self class];
object_setClass(self, superClass);
}
复制代码
这样我们就能保证在进行方法交换的时候,dealloc
方法是一定存在的;
最终代码如下:
接下来,我们来验证一下:
我们在ViewController
的dealloc
方法中不执行ck_removeObserver
方法的时候,此时打印self.person
,我们发现他指向的其实还是动态子类
;我们继续运行断点;
我们发现
ck_dealloc
方法指向之前,self
依然指向动态子类,当ck_dealloc
执行完毕之后,self
指向了Person
,isa
重新指向本类;这样就实现了KVO
的自动销毁;