美文网首页
深入浅出 KVO

深入浅出 KVO

作者: 我叫王可可 | 来源:发表于2019-03-07 11:12 被阅读0次

    前言
    KVO(key value observing) 键值监听是我们在开发中常使用的用于监听特定对象属性值变化的方法,常用于监听数据模型的变化.

    KVO 常用方法

     /*
    注册监听器
    observer: 监听器对象为observer,被监听对象为消息的发送者即方法的调用者在回调函数中会被回传
    keyPath: 监听的属性路径为keyPath支持点语法的嵌套
    options: 监听类型为options支持按位或来监听多个事件类型
    context: 监听上下文context主要用于在多个监听器对象监听相同keyPath时进行区分
    添加监听器只会保留监听器对象的地址,不会增加引用,也不会在对象释放后置空,因此需要自己持有监听对象的强引用,该参数也会在回调函数中回传
    */
    /*
    opitions 参数: 一个枚举类型
        NSKeyValueObservingOptionNew 读取新值,默认值接收新值
        NSKeyValueObservingOptionOld  接收旧值
        NSKeyValueObservingOptionInitial  在注册时接收一次回调,在改变时也会发送通知
        NSKeyValueObservingOptionPrior 改变前发一次,改变之后再发一次
    */
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
    /*
    删除监听器
    observer: 监听器对象为observer,被监听对象为消息的发送者即方法的调用者,应与addObserver方法匹配
    keyPath: 监听的属性路径为keyPath,应与addObserver方法的keyPath匹配
    context: 监听上下文context,应与addObserver方法的context匹配
    */
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
    
    /*
    与上一个方法相同,只是少了context参数
    推荐使用上一个方法,该方法由于没有传递context可能会产生异常结果
    */
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    
    /*
    监听器对象的监听回调方法
    keyPath即为监听的属性路径
    object为被监听的对象
    change保存被监听的值产生的变化
    context为监听上下文,由add方法回传
    */
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
    
    

    KVO 简单实现

    • 通过 addObserver: forKeyPath: options: context: 方法来注册观察者,观察者是可以接收 keyPath 属性的变化事件的.

    • 在观察者中实现 observeValueForKeyPath: ofObject: change: context: 方法, 当 keyPath 属性发生改变后, KVO 会回调这个方法来通知观察者.

    • 当观察者不需要监听了,可以调用 removeObserver:forKeyPath: 方法将 KVO 移除, 此处需要注意的是调用 removeObserver 需要在观察者消失之前,否则会导致 Crash.

    注意:
    在调用 addObserver 方法时, KVO不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放后带来的 Crash .

    比如说下面代码中的 viewController 被销毁了, 因为 p 对象没有对 self 进行强引用, 所以 p 对象就拿不到 self 了, 但是 p 对象还是会调用 observeValueForKeyPath: ofObject: change: context: 方法, 就会导致 Crash .

    演示代码:

    *********************Person类***************
    @interface Person : NSObject
    /**名字*/
    @property (nonatomic, copy) NSString *name;
    
    @end
    ********************************************
    
    @interface ViewController ()
    @property (nonatomic, strong) Person *p;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        _p = [Person new];
        //注册
        [_p addObserver:self forKeyPath:NSStringFromSelector(@selector(name)) options:(NSKeyValueObservingOptionNew) context:nil];
    }
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        static int i = 0;
        _p.name = [NSString stringWithFormat:@"%d", i++];
    }
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"%@", change);
    }
    
    - (void)dealloc {
        [self removeObserver:self forKeyPath:@"name"];
    }
    
    @end
    
    *********************打印台信息***************
    {
        kind = 1;
        new = 0;
    }
    {
        kind = 1;
        new = 1;
    }
    {
        kind = 1;
        new = 2;
    }
    

    KVO 触发模式

    KVO 在属性发生改变时的调用是自动的,如果想手动控制这个调用时机,可以通过 KVO 提供的方法进行调用.

    @implementation Person
    /**模式调整*/
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"name"]) {
            return NO;
        }
        return YES;
    }
    @end
    ********************************************
    
    @interface ViewController ()
    @property (nonatomic, strong) Person *p;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        _p = [Person new];
        //注册
        [_p addObserver:self forKeyPath:NSStringFromSelector(@selector(name)) options:(NSKeyValueObservingOptionNew) context:nil];
        
    }
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        static int i = 0;
        [_p willChangeValueForKey:NSStringFromSelector(@selector(name))];
        _p.name = [NSString stringWithFormat:@"%d", i++];
        [_p didChangeValueForKey:NSStringFromSelector(@selector(name))];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"%@", change);
    }
    
    - (void)dealloc {
        [self removeObserver:self forKeyPath:@"name"];
    }
    
    @end
    

    KVO 属性依赖

    使用场景

    Person 类中有一个 Dog 属性,需要监听 Dog 中的 age 属性

    演示代码

    *********************Dog类***************
    @interface Dog : NSObject
    /**年龄*/
    @property (nonatomic, assign) int age;
    @end
    *********************Person类***************
    @interface Person : NSObject
    /**名字*/
    @property (nonatomic, copy) NSString *name;
    /**dog*/
    @property (nonatomic, strong) Dog *dog;
    
    @end
    - (instancetype)init {
        if (self = [super init]) {
            _dog = [Dog new];
        }
        return self;
    }
    
    /**模式调整*/
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"name"]) {
            return NO;
        }
        return YES;
    }
    ********************************************
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        _p = [Person new];
        //注册
        [_p addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
        
    }
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        static int i = 0;
        [_p willChangeValueForKey:NSStringFromSelector(@selector(name))];
        _p.dog.age = i++;
        [_p didChangeValueForKey:NSStringFromSelector(@selector(name))];
    }
    

    思考:
    如果 Dog 中有多个属性都需要监听, 注册时按照当前的写法就要写出很多 @"dog.age", @"dog.level"等, 哪有有没有更加简洁的办法呢?

    *********************Person类***************
    /**关联需要监听的属性*/
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"dog"]) {
            keyPaths = [[NSSet alloc]initWithObjects:@"_dog.age", @"_dog.level", nil];
        }
        return keyPaths;
    }
    

    KVO 原理探究

    KVO 底层原理
    
    1. 创建一个子类 (命名规则是NSKVONotifying_xxx的格式,代码中就是NSKVONotifying_Person)
    
    2. 重写 setter 方法
    
    3. 外界改变 isa 指针
    
    

    运行截图

    注册观察者之前p的isa指针 注册观察者之后p的isa 指针

    通过打印结果,可以看出在添加 KVO 观察者之后 p 的类对象变成了 NSKVONotifying_Person , 虽然 p 的类对象变成了 NSKVONotifying_Person ,但是我们在调用的时候感觉 p 的类对象还是 Person . 所以推测 KVO 会在运行时创建一个新类,将对象的 isa 指向新创建类, 新类是原类的子类,命名规则是 NSKVONotifying_xxx 的格式.

    自定义 KVO

    #import "NSObject+STKVO.h"
    #import <objc/message.h>
    @implementation NSObject (STKVO)
    - (void)ST_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
        //1. 创建一个子类
        NSString *oldClassName = NSStringFromClass(self.class);
        NSString *newClassName = [@"STKVO_" stringByAppendingString:oldClassName];
        Class myClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
        /**注册类*/
        objc_registerClassPair(myClass);
        
        //2. 重写 setter 方法 : 给子类添加一个setter方法
        
        /**
         参数
    
         @param class 给哪个类添加方法
         @param SEL 方法编号
         @param IMP 方法实现(函数指针)
         @param type 返回值类型
         @return 返回值类型
         */
        class_addMethod(myClass, @selector(setName:), (IMP)setName, "v@:@"); //下面截图是对最后一个参数的官方说明
        
        //3. 外界改变isa指针
        object_setClass(self, myClass);
        
        //4. 将观察者保存到当前对象
        objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_ASSIGN);
        
        
    }
    
    void setName(id self, SEL _cmd, NSString *newName)
    {
        NSLog(@"来了!!");
        /**调用父类的setName方法*/
        Class class = [self class];//拿到当前类型
        object_setClass(self, class_getSuperclass(class));
        objc_msgSend(self, @selector(setName:), newName);
        /**观察者*/
        id observer = objc_getAssociatedObject(self, @"observer");
        if (observer) {
            objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"new": newName, @"kind": @1}, nil);
        }
        /**改回子类*/
        object_setClass(self, class);
    }
    @end
    
    
    image.png Type Encoding

    KVO对容器类的监听

    面试题

    1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

    • 利用Runtime 动态生成一个子类 NSKVONotifying_XXX,并且让 instance 对象的 isa 指向这个全新的子类 NSKVONotifying_XXX.
    • 当修改对象的属性时,会在子类 NSKVONotifying_XXX 调用 Foundation_NSSetXXXValueAndNotify 函数
    • _NSSetXXXValueAndNotify 函数中依次调用 :
      • willChangeValueForKey
      • 父类原来的setter
      • didChangeValueForKey,didChangeValueForKey:内部会触发监听器(Observer)的监听方法( observeValueForKeyPath:ofObject:change:context:)

    2、如何手动触发KVO方法 ?
    手动调用 willChangeValueForKeydidChangeValueForKey 方法
    键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangeValueForKey。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了

    有人可能会问只调用didChangeValueForKey方法可以触发KVO方法,其实是不能的,因为willChangeValueForKey: 记录旧的值,如果不记录旧的值,那就没有改变一说了

    3、直接修改成员变量会触发KVO吗?
    不会触发 KVO,因为 KVO 的本质就是监听对象有没有调用被监听属性对应的 setter 方法,直接修改成员变量,是在内存中修改的,不走 set 方法

    4、不移除KVO监听,会发生什么?

    • 不移除会造成内存泄漏
    • 但是多次重复移除会崩溃。系统为了实现 KVO,为 NSObject 添加了一个名为 NSKeyValueObserverRegistration的CategoryKVOaddremove 的实现都在里面。在移除的时候,系统会判断当前 KVOkey 是否已经被移除,如果已经被移除,则主动抛出一个 NSException 的异常

    相关文章

      网友评论

          本文标题:深入浅出 KVO

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