美文网首页iOS进阶OC基础
KVO 本质 & 自定义实现

KVO 本质 & 自定义实现

作者: xiaoyouPrince | 来源:发表于2020-06-18 16:56 被阅读0次

    KVO 本质 & 自定义实现

    KVO 是什么?

    Key-Value Observer 即键值观察者。作用为监听某个对象的某个属性的值发生改变,通知其观察者,观察者可以做出相应的处理。

    KVO 的使用方法很简单,代码如下:

    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        XYPerson *p1 = [XYPerson new];
        XYPerson *p2 = [XYPerson new];
        
        // 1.设置监听
        [p2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
        
        // 触发
        p1.age = 10;
        p2.age = 20;
        
        // 3.移除监听
        [p2 removeObserver:self forKeyPath:@"age"];
    }
    
    // 2.收到监听
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        NSLog(@"keyPath = %@",keyPath);     // keyPath = age
        NSLog(@"object = %@",object);       // object = <XYPerson: 0x6000010d68b0>
        NSLog(@"change = %@",change);       // change = { kind = 1; new = 20;}
        NSLog(@"context = %@",context);     // context = (null)
    }
    

    内部实现原理

    15919555920910.jpg
    • 系统监听到 addObserver:forKeyPath:options:context: 消息的接受者 p2,会根据其类XYPerson动态创建一个子类 NSKVONotifying_XYPerson,并将 p2->isa 指向其子类
    • 子类NSKVONotifying_XYPerson内部重写了 KeyPath 的 set 方法, 其实现思路如下
    [self willChangeValueForKey:@"age"];  // 发生改变前
    [super setAge:age];
    [self didChangeValueForKey:@"age"]; // 发生改变之后,内部会实现对 observer 的回调
    
    • 系统会重写子类的class 方法,隐藏动态创建的子类,在使用者看来p2指向的还是 XYPerson
    • 调用移除监听方法,会将 p2->isa 重新指向自己原来的类
    添加KVO之前: 
    p2 : <XYPerson: 0x600002ce4530>
    p2->isa : XYPerson
    
    添加KVO之后:
    p2 : <XYPerson: 0x600002ce4530>     // 因为运行时,修改了内部的 class 实现,屏蔽真实类
    p2->isa : NSKVONotifying_XYPerson
    
    移除KVO之后: 
    p2 : <XYPerson: 0x600002ce4530>
    p2->isa : XYPerson
    

    自定义实现 KVO

    系统 KVO 好用,但是其写法为固定的三段式,且代码比较分散(见上面 KVO 示例代码)。

    熟悉了 KVO 的原理,就可以自己基于 runtime 来实现一个简单的 KVO。直接上代码

    /// 一行代码添加KVO监听
    /// @param key 需要被监听的key
    /// @param options 一个混合选项,在 readonly 属性中与系统同效果
    /// @param handler 监听回调,需注意 block 循环引用
    - (void)xy_addObserverForKey:(NSString *)key
                         options:(NSKeyValueObservingOptions)options
                 observerHandler:(xy_observerHandler)handler;
    

    这个 KVO 可以实现一行代码添加 KVO 监听,回调直接在 block 中执行,且无需手动执行移除监听。监听关系会在内部通过 runtime 与 被观察对象进行绑定。当被监听对象销毁的时候,自行销毁。

    具体实现如下:

    - (void)xy_addObserverForKey:(NSString *)key options:(NSKeyValueObservingOptions)options observerHandler:(xy_observerHandler)handler
    {
        // 1. 校验
        SEL setterSEL = [self selWithKey:key];
        Method setterMethod = class_getInstanceMethod(self.class, setterSEL);
        if (!setterMethod) {
            // 没有相应的 setter 方法实现。 如 WKWebView.title
            // 此处为 readonly 方法,具体实现可以看 Demo
            return;
        }
        
        // 2. 创建子类
        Class clz = object_getClass(self);
        NSString *clzName = NSStringFromClass(clz);
        if (![clzName containsString:clzPre]) {
            clz = [self creatKVOClassWithOriginalClassName:clzName];
            
            // 设置成子类
            object_setClass(self, clz);
        }
        
        // 3. 添加方法,仅添加一次
        if (![self hasSeleter:setterSEL]) {
            const char *types = method_getTypeEncoding(setterMethod);
            BOOL s = class_addMethod(clz, setterSEL, (IMP)kvo_setter, types);
            NSLog(@"%d",s);
        }
        
        // 4. 保存KVO相关内容
        KVOInfomation *info = [[KVOInfomation alloc] initWithObserver:self key:key opeitons:options handler:handler];
        NSMutableArray *infos = objc_getAssociatedObject(self, xy_infosKey);
        if (!infos) {
            infos = [NSMutableArray array];
            objc_setAssociatedObject(self, xy_infosKey, infos, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        [infos addObject:info];
    }
    

    第一步校验此 Key 是否为 readonly 的属性,如果是 readonly 属性则没有 setter 方法,就无法通过创建子类监听 setter 然后代用父类 setter 的方式实现。系统的实现应该是内部做了兼容,当加载到被监听的 Key 时候主动调用了监听器方法。

    /// 通过key获取父类的 set 方法
    /// @param key 监听的key
    - (SEL)selWithKey:(NSString *)key{
        // 父类是否存在此set方法
        NSString *selName = [NSString stringWithFormat:@"%@%@%@",@"set",[self xy_firstCharUpper:key],@":"];
        SEL sel = NSSelectorFromString(selName);
        return sel;
    }
    - (NSString *)xy_firstCharUpper:(NSString *)string
    {
        if (string.length == 0) return string;
        NSMutableString *stringM = [NSMutableString string];
        [stringM appendString:[NSString stringWithFormat:@"%c", [string characterAtIndex:0]].uppercaseString];
        if (string.length >= 2) [stringM appendString:[string substringFromIndex:1]];
        return stringM;
    }
    

    第二步创建子类,先判断当前对象是否已经是子类,如果是首次就创建子类并将当前对象指向其子类类型。

    /// 通过本类创建KVO监听的子类
    - (Class)creatKVOClassWithOriginalClassName:(NSString *)originalClassName{
        
        NSString *originalClzName = originalClassName;
        
        NSString *newClassName = [clzPre stringByAppendingString:originalClzName];
        if (objc_getClass(newClassName.UTF8String)) {
            return NSClassFromString(newClassName);
        }
        
        Class newClass = objc_allocateClassPair(self.class, [newClassName UTF8String], 0);
        
        // 重写class 方法,指向父类,隐藏kvo子类
        const char *types = method_getTypeEncoding(class_getInstanceMethod(newClass, @selector(class)));
        class_addMethod(newClass, @selector(class), (IMP)kvo_class, types);
        
        objc_registerClassPair(newClass);
        
        return newClass;
    }
    
    // 重写子类的 class 实现,屏蔽其子类的存在
    static Class kvo_class(id self, SEL _cmd)
    {
        return class_getSuperclass(object_getClass(self));
    }
    

    第三步给子类添加被监听Key的 setter 方法实现,其内部调用父类的 setter 方法,并执行监听回调

    static void kvo_setter(id self, SEL _cmd, id newValue)
    {
        // 给父类发送修改值的消息
        NSString *setterName = NSStringFromSelector(_cmd);
        NSString *getterName = [self getterFromSetterName:setterName];
        if (!getterName) {
            NSLog(@" ----- 原来类中没有 getter 方法");
        }
        
        id oldValue = [self valueForKey:getterName];
        
        // 调用父类setter
        // 构造 objc_super 的结构体
        struct objc_super superclazz = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self)),
        };
        
        // 对 objc_msgSendSuper 进行类型转换,解决编译器报错的问题
        void (* objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
        
        // id objc_msgSendSuper(struct objc_super *super, SEL op, ...) ,传入结构体、方法名称,和参数等
        objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
        
        // 回调 handler - 依据options
        NSMutableArray *infos = objc_getAssociatedObject(self, xy_infosKey);
        for (KVOInfomation *info in infos) {
            if ((info.observer == self) && ([info.key isEqualToString:getterName])) {
                NSDictionary *dict = @{
                    @"observeredObj": info.observer,
                    @"key": info.key,
                    NSKeyValueChangeNewKey: newValue?:@"",
                    NSKeyValueChangeOldKey: oldValue?:NSNull.null
                };
                info.handler(dict);
            }
        }
    }
    

    第四步保存添加的监听信息,当监听到某个 Key 发生回调之后,可以去内部缓存中去调用回调。

    关于监听关系类的设计如下,其属性均为 readonly, 规避了一些循环引用问题。

    @interface KVOInfomation : NSObject
    /** observer: 被观察对象 */
    @property (nonatomic, readonly,weak)       NSObject * observer;
    /** key */
    @property (nonatomic, readonly,copy)         NSString *key;
    /** options */
    @property (nonatomic, readonly,assign)       NSKeyValueObservingOptions options;
    /** key */
    @property (nonatomic, readonly,copy)         xy_observerHandler handler;
    
    - (instancetype)initWithObserver:(id)observer key:(NSString *)key opeitons:(NSKeyValueObservingOptions)options handler:(xy_observerHandler)handler;
    
    @end
    

    代码就写到这里了,有兴趣的可以看一下全部 Demo

    祝大家学习进步~

    相关文章

      网友评论

        本文标题:KVO 本质 & 自定义实现

        本文链接:https://www.haomeiwen.com/subject/trguxktx.html