IOS-自定义KVO

本文首发于 个人博客

之前写了一篇 kvo原理探索,今天我们来自己动手实现它的逻辑,这样我们对kvo的认识会更深刻一些。
但是为了更加完善一些,我们又添加了automatically方法、ss_willChangeValueForKey:ss_didChangeValueForKey:等方法,真正实现手自一体

接口定义

根据系统KVO的相关接口,我们也给NSObject添加一个Category,具体的头文件我们定义如下:

typedef NS_OPTIONS(NSUInteger, SSKeyValueObservingOptions) {
    SSKeyValueObservingOptionNew = 0x01,
    SSKeyValueObservingOptionOld = 0x02,
};

@interface NSObject (SSKVO)

/** Register an observer of the value at a key path relative to the receiver */
- (void)ss_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(SSKeyValueObservingOptions)options context:(nullable  void *)context;

/* deregister as an observer of the value at a key path relative to the receiver*/
- (void)ss_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable  void *)context;

/*  Given that the receiver has been registered as an observer of the value at a key path relative to an object, be notified of a change to that value */
- (void)ss_observeValueForKeyPath:(nullable  NSString *)keyPath ofObject:(nullable  id)object change:(nullable  NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable  void *)context;

+ (BOOL)ss_automaticallyNotifiesObserversForKey:(NSString *)key;

- (void)ss_willChangeValueForKey:(NSString *)key;

- (void)ss_didChangeValueForKey:(NSString *)key;

@end

接口实现

接口定义完了之后,我们就针对定义的接口方法一一去实现逻辑。

观察者包装

由于我们需要保存观察者的相关信息以便于在后续的设值和传值更加方便,此处我们封装一个SSKVOInfo的对象,考虑到一个对象有多个属性值可能会被观察,我们创建一个以keyPath作为键值的一个字典,保存了多有keyPath的观察者信息。

@interface SSKVOInfo : NSObject {
    @public
    id observer;
    void *context;
    int options;
    NSString *keyPath;
    NSDictionary *changes;
}
@end

@implementation SSKVOInfo

@end

SSKVOInfo相关方法如下:

- (SSKVOInfo *)kvoInfoForKey:(NSString *)key {
    NSMutableDictionary *observerInfo = objc_getAssociatedObject(self, (__bridge const  void * _Nonnull)(SSKVOAssiociateKey));
    id object = [observerInfo objectForKey:key];
    if (object && [object isKindOfClass:[SSKVOInfo class]]) {
        return object;
    }
    return  nil;
}
- (void)addKVOInfo:(SSKVOInfo *)kvoInfo forKey:(NSString *)key {
    NSMutableDictionary *observerInfo = objc_getAssociatedObject(self, (__bridge const  void * _Nonnull)(SSKVOAssiociateKey));
    if (!observerInfo) {
        observerInfo = [NSMutableDictionary dictionary];
    }
    [observerInfo setObject:kvoInfo forKey:key];
    objc_setAssociatedObject(self, (__bridge const  void * _Nonnull)(SSKVOAssiociateKey), observerInfo, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

通知回调

什么时候发送通知呢???
/** 重写set方法 */
void ss_setter(id self, SEL _cmd, id value)
{
    NSString *v = NSStringFromSelector(_cmd);
    NSString *key = getterForSetter(v);
    Class c = [self  class];
    /** 自动观察,我们自动调用ss_willChangeValueForKey 和  ss_didChangeValueForKey*/
    if ([c ss_automaticallyNotifiesObserversForKey:key]) {
        [self ss_willChangeValueForKey:key];
        /** 调用父类的set方法 */
        ss_sendSuper(self, _cmd, value);
        [self ss_didChangeValueForKey:key];
    } else {// 只调用父类方法,如果父类自己实现will和did方法,也会发送通知
        ss_sendSuper(self, _cmd, value);
    }
}

其关联的手动KVO实现如下:

/** 调用父类的set方法 */
void ss_sendSuper(id self, SEL _cmd, id value)
{
    void (*ss_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)
    ss_msgSendSuper(&superStruct,_cmd,value);
}

- (void)ss_willChangeValueForKey:(NSString *)key {
    [self saveCurrentValueFor:SSKVO_Old keyPath:key];
}

- (void)ss_didChangeValueForKey:(NSString *)key {
    [self saveCurrentValueFor:SSKVO_New keyPath:key];
    [self sendNotification:key];
} 

- (void)saveCurrentValueFor:(NSString *)changeKey  keyPath:(NSString *)keyPath {
    id object = [self kvoInfoForKey:keyPath];
    if (!object || ![object isKindOfClass:[SSKVOInfo class]]) {return;}
    SSKVOInfo *kvoInfo = (SSKVOInfo *)object;
    NSMutableDictionary *changes = kvoInfo->changes.mutableCopy;
    id value = [self valueForKey:keyPath]?[self valueForKey:keyPath]:@"";
    [changes setObject:value forKey:changeKey];
    kvoInfo->changes = changes.copy;
}

- (void)sendNotification:(NSString *)keyPath {
    id object = [self kvoInfoForKey:keyPath];
    if (!object || ![object isKindOfClass:[SSKVOInfo class]]) {return;}
    SSKVOInfo *kvoInfo = (SSKVOInfo *)object;
    // send notification

    id observer = kvoInfo->observer;
    // 发送通知

    if (observer && [observer respondsToSelector:@selector(ss_observeValueForKeyPath:ofObject:change:context:)]) {
        int options = kvoInfo->options;
        NSDictionary *changes = kvoInfo->changes;
        NSMutableDictionary *info = [NSMutableDictionary dictionaryWithCapacity:2];
        if (options & SSKeyValueObservingOptionNew) {
            [info setObject:changes[SSKVO_New] forKey:SSKVO_New];
        }
        if (options & SSKeyValueObservingOptionOld) {
            [info setObject:changes[SSKVO_Old] forKey:SSKVO_Old];
        }
        [observer ss_observeValueForKeyPath:keyPath ofObject:self change:info context:kvoInfo->context];
    }
}

移除观察者

- (void)ss_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable  void *)context {
    [self removeKVOInfoForKey:keyPath];
}
- (void)removeKVOInfoForKey:(NSString *)key {
    NSMutableDictionary *observerInfo = objc_getAssociatedObject(self, (__bridge const  void * _Nonnull)(SSKVOAssiociateKey));
    [observerInfo removeObjectForKey:key];
    objc_setAssociatedObject(self, (__bridge const  void * _Nonnull)(SSKVOAssiociateKey), observerInfo, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    if (observerInfo.count<=0) {
        // 指回给父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

调试

代码逻辑实现了,必不可少的就是测试了。

这是自动挡,接下来我们试试手动挡如何,调整SSPerson的实现如下:

#import "SSPerson.h"
#import "NSObject+SSKVO.h"

@implementation SSPerson

+ (BOOL)ss_automaticallyNotifiesObserversForKey:(NSString *)key {
    return   NO;
}

- (void)setName:(NSString *)name {
    [self ss_willChangeValueForKey:NSStringFromSelector(@selector(name))];
    _name = @"here is custom value";
    [self ss_didChangeValueForKey:NSStringFromSelector(@selector(name))];
}

@end

运行同样点击两次屏幕我们看看打印结果是否为here is custom value

总结

至此我们大概完成了自定义kvo的实现,上述只展示了一些主要的核心逻辑,具体的代码需要的话请前往SSKVO-demo下载,如果说这份代码不够缜密,那是当然的,比如我们只处理了对象类型的属性观察缺少了基本类型的处理,比如我们的keyPath只是针对key并没有path的处理,还有针对KVC是如何触发KVO的实现等等,这些后续我都会去完善,也希望您能多多赐教和点评。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • KVO原理及使用 我们之前讨论过KVO的原理,知道KVO机制是生成了一个中间类NSKVONotifying,该中间...
    xxxxxxxx_123阅读 358评论 0 0
  • 自己实现kvo之前,需要知道iOS系统对kvo的实现。 系统实现kvo的原理 这依赖了OC强大的runtime特性...
    mws100阅读 2,802评论 6 3
  • 之前我们已经了解过了KVO的底层实现原理,不过呢,在我们开始实现自定义KVO之前再来简单回顾下KVO的实现原理 1...
    ForScanf阅读 259评论 0 0
  • 利用Runtime 实现简单的自定义kvo 代码githubgithub.com/zswj/custom-KVO ...
    我是数据链路层阅读 877评论 1 0
  • 通过在了解KVO的实现原理和实现步骤之后,我们可以手动实现KVO,具体可以看最后的demo,这里只讲实现原理 添加...
    wp_Demo阅读 434评论 0 2