自己实现KVO(block)
2021-07-13 本文已影响0人
晨曦中的花豹
先向先驱者致敬[https://tech.glowing.com/cn/implement-kvo/]
看了我之前的仿系统实现KVO自己实现KVO(代理)的同学,看这个应该不会太费劲,但是其中有几个优化点和注意的问题
- 是考虑多对象监听同一个对象向(不能创建多个子类,一监听的属性不能重复关联方法)
- 考虑多个对象监听同一个类的不同对象
- 移除监听时有个小插曲(就是用weak修饰的observer弱引用被提前置为nil,导致无法对比移除对应监听)
下面是我在前者之上做了具体的分析
包含两部分kvo_block分类
和LXCObservationInfo block保存对象
kvo_block分类
//
// NSObject+kvo_block.m
// leetCode
//
// Created by 刘晓晨 on 2021/7/13.
//
#import "NSObject+kvo_block.h"
static NSString *kvo_class_prefix = @"KVOClass_";
const void *kvo_observers = &kvo_observers;
@implementation NSObject (kvo_block)
- (void)lxc_addObserver:(NSObject *)observer forKey:(NSString *)key block:(AddObserverBlock)block {
// 1.判断属性是否存在
SEL setterSelector = NSSelectorFromString(setterFromGetter(key));
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
if (!setterMethod) {
// 报异常,方便查找问题
NSString *reason = [NSString stringWithFormat:@"%@没有%@对应的setter", self, key];
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
}
//原始类
Class oldClass = object_getClass(self);
// 拿到原始类名
NSString *oldClassName = NSStringFromClass(oldClass);
//这里的判断主要是为了判断当前被监听对象是否已经被监听过,如果有被监听,则class名称带kvo前缀,不用重新创建子类,使用当前类即可
if (![oldClassName hasPrefix:kvo_class_prefix]) {
Class kvoClass = [self makeKvoClassWithOriginalClassName:oldClassName];
// 3.修改isa指针,使得self->isa指向子类
object_setClass(self, kvoClass);
}
//判断是否属性关联到kvo_setter上,比如name,第一次监听name需要将sel与kvo_setter关联上,而第二次监听则不需要再次关联,但是实际发现重复添加并无问题
IMP imp = class_getMethodImplementation(object_getClass(self), setterSelector);
if (imp != (IMP)kvo_setter) {
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(object_getClass(self), setterSelector, (IMP)kvo_setter, types);
}
// 5.保存block及用于筛选的参数key和observer(这两个参数主要是在remove时候筛选使用)
LXCObservationInfo *info = [[LXCObservationInfo alloc] init];
info.block = block;
info.key = key;
info.observer = observer;
//这里创建数组是因为同一个对象可能会被多个地方监听,这个时候需要去执行多个block
NSMutableArray *observers = objc_getAssociatedObject(self, kvo_observers);
if (!observers) {
//创建保存监听对象的数组
observers = [NSMutableArray array];
objc_setAssociatedObject(self, kvo_observers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[observers addObject:info];
}
-(void)lxc_removeObserver:(NSObject *)observer forKey:(NSString *)key {
NSMutableArray *observers = objc_getAssociatedObject(self, kvo_observers);
for (LXCObservationInfo *observerInfo in observers) {
/*
这里判断如果监听对象一致,监听属性一致,及删除
此处遇到了一个颠覆我认知的一个问题如果在info中我用weak来修饰observer,这个时候这里observerInfo.observer是获取不到的,因为调用这个方法一般都是在dealloc中,所以我认为所有指向自己的弱引用指针被置为nil是发生在dealloc之前,是不是很不可思议,但是实践证明确实如此,所以在info中我改为用assign修饰,因为此时外部传入的observer是有值的,所以assign修饰不会出问题
*/
if (observerInfo.observer == observer && [observerInfo.key isEqualToString:key]) {
[observers removeObject:observerInfo];
break;
}
}
}
//创建子类
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName {
NSString *kvoClazzName = [kvo_class_prefix stringByAppendingString:originalClazzName];
Class clazz = NSClassFromString(kvoClazzName);
if (clazz) {
return clazz;
}
Class originalClazz = object_getClass(self);
//让新的Class继承自原始类
Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
//仿苹果隐藏子类(及重写本类的class对象方法)
Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
const char *types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
//注册子类
objc_registerClassPair(kvoClazz);
return kvoClazz;
}
//重写class方法
Class kvo_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
//统一管理setter方法
void kvo_setter(id self, SEL _cmd, id newName)
{
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = getterFromSetter(setterName);
id oldValue;
if (getterName) {
oldValue = [self valueForKey:getterName];
}
// 获取kvo类型
id class = object_getClass(self);
// 调用父类的方法(此处还有一种方式是修改self isa 指向原始类,修改后在修改为 子类,这里使用的是系统实现super的方式,顺便可以了解下super和self的区别)
Class super_class = class_getSuperclass(class);
struct objc_super * _Nonnull super_struct = malloc(sizeof(struct objc_super));
super_struct->receiver = self;
super_struct->super_class = super_class;
objc_msgSendSuper(super_struct, _cmd,newName);
id newValue = [self valueForKey:getterName];
NSMutableArray *observers = objc_getAssociatedObject(self, kvo_observers);
for (LXCObservationInfo *observer in observers) {
if ([observer.key isEqualToString:getterName]) {
observer.block(oldValue, newValue);
}
}
}
//通过属性获取setter字符串
NSString* setterFromGetter(NSString *key) {
if (key.length > 0) {
NSString *resultString = [key stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[key substringToIndex:1] capitalizedString]];
return [NSString stringWithFormat:@"set%@:",resultString];
}
return nil;
}
//通过setter 获取getter
NSString* getterFromSetter(NSString *key) {
if (key.length > 0) {
NSString *resultString = [key substringFromIndex:3];
resultString = [resultString substringToIndex:resultString.length - 1];
return [resultString lowercaseString];
}
return nil;
}
@end
LXCObservationInfo block保存对象
//
// LXCObservationInfo.h
// leetCode
//
// Created by 刘晓晨 on 2021/7/9.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef void(^AddObserverBlock)(id oldValue, id newValue);
@interface LXCObservationInfo : NSObject
@property (nonatomic,assign)id observer;
@property (nonatomic,copy)NSString *key;
@property (nonatomic,copy)AddObserverBlock block;
@end
NS_ASSUME_NONNULL_END
最后是小插曲的验证
A弱引用B,B实现block(其中打印A弱引用的B),在B的dealloc中调用block,发现block执行,A弱引用的B竟然是nil
A中
@property (nonatomic,weak)TwoViewController *two;
self.two = vc;
__weak typeof(self) weakSelf = self;
vc.block = ^{
NSLog(@"twoblock%@",weakSelf.two);
};
B中
-(void)dealloc {
self.block();
}
最终打印
2021-07-13 18:52:28.715064+0800 leetCode[74996:13935331] twoblock(null)
ok关于KVO的内容到此结束,因为weak引用的问题,之后我会更新一版weak的知识点,希望大家多多支持