iOS自定义KVO(二)-多属性观察及销毁

1,461 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

自定义KVO的多元素观察和移除观察

多元素观察

在上文我们已经实现了针对属nickName的监听,那么如果我们需要监听多个属性呢?比如我们要同时监听Person的两个属性nickNamerealName

@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这个类来保存keyPathobserver等一系列数据;那么在外界我们就需要一个CKKVOInfo的数组集合来收集信息;

代码修改如下: image.png image.png

需要注意的是此处代码:

// 给观察者发送消息
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设置项 image.png 代码可以写为:

// 给观察者发送消息
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;

移除观察者

image.png

  • 每一次移除一个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方法是一定存在的;

最终代码如下: image.png

接下来,我们来验证一下: image.png

我们在ViewControllerdealloc方法中不执行ck_removeObserver方法的时候,此时打印self.person,我们发现他指向的其实还是动态子类;我们继续运行断点; image.png image.png 我们发现ck_dealloc方法指向之前,self依然指向动态子类,当ck_dealloc执行完毕之后,self指向了Personisa重新指向本类;这样就实现了KVO的自动销毁;