KVO探索及自定义KVO的实现

1,496 阅读5分钟

1. KVO是什么

KVO Apple官方文档

KVO(Key-Value Observing): Objective-C对观察者设计模式的一种实现.键-值观察是一种机制,允许在其他对象的指定属性发生更改时通知对象.

2. KVO底层实现的原理

Key-Value Observing Implementation Details(官方文档)

Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch t able essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

    1. KVO是基于runtime实现的isa-swizzling技术实现的
    1. 当某个对象被注册为观察者时, 系统就会在运行期动态地创建该类的一个派生类NSKVONotifying_XX,对象的isa指针被修改,指向派生类,而不是真正的类
    1. 在这个派生类中重写呗观察属性的setter方法,派生类在呗重写的setter方法内实现真正的通知机制
    1. KVO的通知主要基于willChangeValueForKeydidChangevlueForKey这两个方法. willChangeValueForKey值被改变之前调用这个方法,记录下oldValue, 发生改变之后,调用didChangevlueForKey方法,然后调用observeValueForKey:ofObject:change:context:方法
    1. KVO这套实现当中还重写了class,从而达到隐藏派生类

3. KVO底层实现的论证

3.1 准备代码

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [Person new];
    //context:(nullable void *) void * 类型 用NULL
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

    self.person.name = @"+";
}

- (IBAction)modifiedValue:(UIButton *)sender {
    self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"监听方法:%@", change);
}

- (IBAction)removeObserve:(UIButton *)sender {
    [self.person removeObserver:self forKeyPath:@"name"];
    NSLog(@"观察者移除成功");
}

执行结果:

监听方法:{
    kind = 1;
    new = "+";
    old = "<null>";
}
监听方法:{
    kind = 1;
    new = "++";
    old = "+";
}
监听方法:{
    kind = 1;
    new = "+++";
    old = "++";
}
  • kind = 1通过查看枚举定义可知NSKeyValueChangeSetting属于setter方法修改的值
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,     //属性的setter方法
    NSKeyValueChangeInsertion = 2,   //集合类型的添加方法
    NSKeyValueChangeRemoval = 3,     //集合的移除操作
    NSKeyValueChangeReplacement = 4, //可变集合类型的元素替换
};

3.2 验证派生类NSKVONotifying_XX

验证派生类

  • 断点1 的时候,我们打印对象的类名得到的是Person
  • 断点2 的时候,我们打印对象的类名得到的是NSKVONotifying_Person
  • 从而验证了在添加完KVO底层会生成一个派生类,

3.3 验证willChangeValueForKeydidChangevlueForKey

准备代码:

// 关闭自动建值观察者
+ (BOOL)accessInstanceVariablesDirectly:(NSString *)key {
    return NO;
}

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:name];
    NSLog(@"setter方法: willChangeValueForKey -%@", _name);
    _name = [name copy];
    [self didChangeValueForKey:name];
    NSLog(@"setter方法: didChangeValueForKey -%@", _name);
}

执行结果:

setter方法: willChangeValueForKey -(null)
setter方法: didChangeValueForKey -+
监听方法:{
    kind = 1;
    new = "+";
    old = "<null>";
}
setter方法: willChangeValueForKey -+
setter方法: didChangeValueForKey -++
监听方法:{
    kind = 1;
    new = "++";
    old = "+";
}

完美验证了我们上面的论证

3.4 移除通知后,isa指向原来的类

通过上图可以得出:KVO移除后,isa又指向原来的类

4. KVO的使用

4.1 KVO观察可变数组

Apple文档-可变数组

KVC官方文档中,针对可变数组的集合类型需要通过mutableArrayValueForKey方法,这样才能将元素添加到可变数组中

self.person.dataArray = [NSMutableArray array];
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"1"];

4.2 KVO:一对多观察

- (NSString *)downloadProgress {
    return [NSString stringWithFormat:@"%f", 1.0f * self.writeData / self.totalData];
}

// 路径处理
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSMutableSet *keyPaths = [[super keyPathsForValuesAffectingValueForKey:key] mutableCopy];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKey = @[@"totalData", @"writeData"];
        [keyPaths setByAddingObjectsFromArray:affectingKey];
    }
    return keyPaths;
}


//2、注册KVO观察
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];


//3、触发属性值变化
self.person.totalData += 1;
self.person.writeData += 1;

//4、移除观察者
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"downloadProgress"];
}

5. 自定义KVO的实现

5.1 注册观察者

    1. 验证当前keyPath是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath {
    Class superClass = object_getClass(self);

    NSString *setterMethodName = setterForGetter(keyPath);
    SEL setterSEL = NSSelectorFromString(setterMethodName);
    Method setterMethod = class_getInstanceMethod(superClass, setterSEL);

    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没有%@的set方法", keyPath] userInfo:nil];
    }
}
    1. 动态生成派生子类KVONotifying_xxx
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@", kKVOPrefix, oldClassName];
    Class newClass = NSClassFromString(newClassName);
    if (newClass) return newClass;
    //2.1 申请类
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    //2.2 注册
    objc_registerClassPair(newClass);
    //2.3 添加方法 属性 -ivar -ro

    // 2.3.1 : 添加class : class的指向是Person
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)kvo_class, classTypes);
    // 2.3.2 : 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)kvo_setter, setterTypes);

    return newClass;
}
    1. 修改isa指向,指向子类
object_setClass(self, newClass);

    1. 保存信息
  //4. 保存信息
    KVOInfo *info = [[KVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
  • 完整注册方法代码
- (void)kvo_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KVOBlock)block {
    //1. 验证setter是否存在
    [self judgeSetterMethodFromKeyPath:keyPath];
    //2. 动态生成之类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3. 修改isa指向 KVONotifying_XX
    object_setClass(self, newClass);

    //4. 保存信息
    KVOInfo *info = [[KVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
}

5.2 响应者

  • setter方法中,将系统objc_msgSendSuper转换成自定义消息发送
  • 通过block告知observer响应
static void kvo_setter(id self, SEL _cmd, id newValue)
{
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    //  消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    void (*kvo_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver    = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    kvo_msgSendSuper(&superStruct, _cmd, newValue);

    // 信息数据回调
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey));

    for (KVOInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

5.3 移除观察者

- (void)kvo_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey));
    if (observerArr.count <= 0) {
        return;
    }

    for (KVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }

    if (observerArr.count <= 0) {
        // 指回给父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

完整代码见GitHub->KVOExplore

参考资料:

FBKVOController Apple文档


如有不足之处,欢迎予以指正, 如果感觉写的不错,记得给个赞呦!