KVO探究

作者: Maj_sunshine | 来源:发表于2018-07-02 13:10 被阅读14次

    KVO原理

    • KVO是基于runtime机制实现的
      当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制
    • 如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
    • 每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法
    • 键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

    弄清为什么改变isa指针的指向,能修改调用的方法。

    • 查看实例对象内部结构
      打开xcode,按command+shift+O搜索objc.h,我们可以看到
    typedef struct objc_class *Class;
    
    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    /// A pointer to an instance of a class.
    typedef struct objc_object *id;
    #endif
    
    /// An opaque type that represents a method selector.
    typedef struct objc_selector *SEL;
    
    /// A pointer to the function of a method implementation. 
    #if !OBJC_OLD_DISPATCH_PROTOTYPES
    typedef void (*IMP)(void /* id, SEL, ... */ ); 
    #else
    typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
    #endif
    

    我们可以看出

    1. Class是个objc_class类型的结构体
    2. 所有实例对象的本质是objc_object类型的结构体,里面存放这个实例对象的isa指针。
    3. id是一个objc_object类型的指针,这应该就是id可以指向任意对象的原因
    4. SEL是个objc_selector结构体类型的指针,存放的是方法名
    5. IMP函数指针,存放方法的具体实现地址。
    • 再来看看 objc_class
    struct objc_class {
        Class _Nonnull isa  // 所属类的指针
    
    #if !__OBJC2__
        Class _Nullable super_class       // 父类
        const char * _Nonnull name      // 类名                         
        long version       // 版本
        long info          // 运行期使用的位标识
        long instance_size            // 实例大小
        struct objc_ivar_list * _Nullable ivars     // 实例变量列表
        struct objc_method_list * _Nullable * _Nullable methodLists    // 方法列表              
        struct objc_cache * _Nonnull cache      // 方法缓存                   
        struct objc_protocol_list * _Nullable protocols     // 协议列表
    #endif
    
    } OBJC2_UNAVAILABLE;
    
    我们可以看出类中也有isa指针,这个isa指针是指向meta元类中。实例对象,类和元类的关系其实有一张图很明显。 0_1326963670oeC1.gif.png

    isa的指向是从实例对象->类对象->元类对象->根元类对象->自己。

    • 当我们向一个实例对象发送消息,即调用了实例方法时,在运行时会顺着isa指针指向的类对象中查找相对应的方法,先从缓存中查找,如果缓存未命中,则从方法列表中查找。如果未找到,则一直顺着父类找到NSObject,如果还未找到,则走消息转发。
    • 当我们向一个类对象发送消息时,即使用类方法调用,在运行时会顺着isa,找到类的元类,在元类中查找类方法。先从缓存中查找,如果缓存未命中,则从方法列表中查找。如果未找到,则一直顺着父元类一直找直到找到NSObject,如果还未找到,则走消息转发。
    那我们可以知道,由isa指针指向的对象才是真正调用方法的对象。在类中存储着实例对象的实例方法,元类中存储着类对象的类方法,KVO中的isa- swizzle就是交换isa的指向,本来是在A类中查找调用setter方法,运行时改成了在B类中查找调用setter方法。

    使用KVO的方法。

    • 添加观察者
    _dog = [[Animal alloc] init];
    [_dog setValue:@"牧尘" forKey:@"name"];
    // 添加KVO
    [_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    
    // 触发KVO
    _dog.name = @"小明";
    
    • KVO的监听
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
            NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
        }
    }
    
    • 通知的移除
    - (void)dealloc {
        [_dog removeObserver:self forKeyPath:@"name"];
    }
    
    • 打印结果
    2018-06-29 13:51:37.175922+0800 KVO与NSKeyValueObserving[38410:509677] 新值为小明
    2018-06-29 13:51:37.176043+0800 KVO与NSKeyValueObserving[38410:509677] 旧值为牧尘
    

    如果是简单使用,那知道这个就可以了。

    使用探究

    • 我们再看下KVO的方法,可以看出这是通过KVO是对键值进行的观察。
    [_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    
    • 那么我们知道KVC访问的方式是通过下面两中方式。
    - (void)setValue:(nullable id)value forKey:(NSString *)key;
    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
    

    能用第一种setValue:forKey:方式访问的肯定也能通过setValue: forKeyPath:方式访问,但是通过keyPath方式访问变量不一定能用第一种方式访问。

    疑问一 :能通过KVO监听对象中另一个对象的属性么?

    新增Food类,实例化为Animal类的对象。

    #import <Foundation/Foundation.h>
    
    @class Food;
    /**
     动物类
     */
    @interface Animal : NSObject
     // 姓名
    @property (nonatomic, strong) NSString *name;
     // 食物 
    @property (nonatomic, strong) Food *food;
    
    @end
    
    
    #pragma mark --- 食物
     // 食物类
    @interface Food : NSObject
     // 水果
    @property (nonatomic, strong) NSString *fruit;
    
    @end
    
    • 对food.fruit进行监听
     [_dog setValue:@"苹果" forKeyPath:@"food.fruit"];
    [_dog addObserver:self forKeyPath:@"food.fruit" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [_dog setValue:@"荔枝" forKeyPath:@"food.fruit"];
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        if ([keyPath isEqualToString:@"food.fruit"]) {
            NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
            NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
        }
    }
    
    • 结果
    2018-06-29 14:13:45.002673+0800 KVO与NSKeyValueObserving[38503:523330] 新值为荔枝
    2018-06-29 14:13:45.002896+0800 KVO与NSKeyValueObserving[38503:523330] 旧值为苹果
    

    可以看出可以通过keyPath的方式对对象的对象进行监听。

    疑问二 : 通过keyPath方式监听,只局限于属性监听么,实例对象不能监听么?
    • 属性可以通过点语法的方式访问变量,会调用setter方法。

    • KVC方式会查看如果有``setter方法,调用setter方式赋值,如果没有setter方法,检查accessInstanceVariablesDirectly。如果accessInstanceVariablesDirectly为YES,顺着_name,_isName,name,isName方式查找实例变量,有则赋值。没有则抛异常。

    • 测试通过实例变量,是不是setter方法能进行KVO的监听
      去除name的属性,新增isName实例变量。

    @interface Animal() {
        NSString *isName;
    }
    @end
    
    @implementation Animal
    
    @end
    
    [_dog setValue:@"牧尘" forKey:@"name"];
    [_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [_dog setValue:@"小飞" forKey:@"name"];
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
            NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
        }
    }
    
    • 结果
    2018-06-29 14:36:51.210152+0800 KVO与NSKeyValueObserving[38565:537594] 新值为小飞
    2018-06-29 14:36:51.210349+0800 KVO与NSKeyValueObserving[38565:537594] 旧值为牧尘
    

    可以看出,KVO监听不仅能通过对属性进行监听,还能对类中的实例变量进行监听。

    • 为什么对实例变量也能监听在文章下说明。

    手动触发KVO

    • NSKeyValueObserving中有一个方法
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
    

    这个方法的含义是否自动对key进行观察,就是自动触发KVO。默认为YES。我们可以重写方法设置为NO,让我们手动触发。

    • 调用
    - (void)willChangeValueForKey:(NSString *)key;
    - (void)didChangeValueForKey:(NSString *)key;
    

    方法来实现对key的手动监听。willChangeValueForKey:顾名思义,将要改变的时候调用的方法,那么didChangeValueForKey:就是已经改变的时候调用的方法,当NSKeyValueObservingOptionNSKeyValueObservingOptionOld| NSKeyValueObservingOptionNew

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        _dog = [[Animal alloc] init];
        [_dog setValue:@"牧尘" forKey:@"name"];
        [_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        
        NSLog(@"即将改变name");
        [_dog willChangeValueForKey:@"name"];
    //  改变值
        [_dog setValue:@"小飞" forKey:@"name"];
    //    _dog.name = @"小明";
        NSLog(@"已经改变name");
        [_dog didChangeValueForKey:@"name"];    
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"调用了observeValueForKeyPath方法");
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"新值为%@",[change objectForKey:NSKeyValueChangeNewKey]);
            NSLog(@"旧值为%@",[change objectForKey:NSKeyValueChangeOldKey]);
        }
    }
    

    打印

    2018-06-29 16:58:38.552276+0800 KVO与NSKeyValueObserving[43395:635717] 即将改变name
    2018-06-29 16:58:38.552577+0800 KVO与NSKeyValueObserving[43395:635717] 已经改变name
    2018-06-29 16:58:38.552752+0800 KVO与NSKeyValueObserving[43395:635717] 调用了observeValueForKeyPath方法
    2018-06-29 16:58:38.552873+0800 KVO与NSKeyValueObserving[43395:635717] 新值为小飞
    2018-06-29 16:58:38.552998+0800 KVO与NSKeyValueObserving[43395:635717] 旧值为牧尘
    
    • 可以看出option值为newold只会在didChangeValueForKey :已经改变值后调用一次。
    当option 为NSKeyValueObservingOptionPrior时
    • 结果
    2018-06-29 17:04:28.115616+0800 KVO与NSKeyValueObserving[43412:639884] 即将改变name
    2018-06-29 17:04:28.115937+0800 KVO与NSKeyValueObserving[43412:639884] 调用了observeValueForKeyPath方法
    2018-06-29 17:04:28.116069+0800 KVO与NSKeyValueObserving[43412:639884] 新值为(null)
    2018-06-29 17:04:28.116329+0800 KVO与NSKeyValueObserving[43412:639884] 旧值为牧尘
    2018-06-29 17:04:28.116471+0800 KVO与NSKeyValueObserving[43412:639884] 已经改变name
    2018-06-29 17:04:28.116589+0800 KVO与NSKeyValueObserving[43412:639884] 调用了observeValueForKeyPath方法
    2018-06-29 17:04:28.116711+0800 KVO与NSKeyValueObserving[43412:639884] 新值为小飞
    2018-06-29 17:04:28.116816+0800 KVO与NSKeyValueObserving[43412:639884] 旧值为牧尘
    
    • 我们可以得出结果- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;方法会在willChangeValueForKey :调用时触发一次,在didChangeValueForKey :调用时触发一次,其中调用willChangeValueForKey后是否触发方法和设置的NSKeyValueObservingOptions有关。

    runtime到底是什么时候修改了对象的isa指针。

    • 重写model类的description方法
    - (NSString *)description {
         // 查看当前isa指针指向的类
        Class runtimeClass = object_getClass(self);
         // 当前类的父类
        Class superClass = class_getSuperclass(runtimeClass);
        NSLog(@"runtimeClass = %@, superClass = %@",runtimeClass, superClass);
        
        return  @"";
    }
    
    • 在添加观察者前后打印代码
    [_dog description];
    [_dog addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    [_dog description];
    
    • 结果
    2018-06-29 18:09:05.005066+0800 KVO与NSKeyValueObserving[43833:680180] runtimeClass = Animal, superClass = NSObject
    2018-06-29 18:09:05.005591+0800 KVO与NSKeyValueObserving[43833:680180] runtimeClass = NSKVONotifying_Animal, superClass = Animal
    

    我们可以看出,KVO在- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法调用后,在运行时动态生成了一个原来类的子类NSKVONotifying_XXX,导致我们通过点语法或者 KVC为属性或者实例变量赋值的时候,调用的是这个isa指向的类NSKVONotifying_XXX里的方法列表中的setter方法。

    回到上面一个问题,实例变量为什么也能监听?

    对实例变量通过KVC访问赋值,由于isa指针的混淆,KVC实际是对NSKVONotifying_XXX类调用。但是由于实例变量无法继承到NSKVONotifying_XXX中,但是KVO仍然会对NSKVONotifying_XXX调用- (void)setValue:(id)value forKey:(NSString *)key方法,NSKVONotifying_XXX调用失败,会调用super类的- (void)setValue:(id)value forKey:(NSString *)key,即赋值成功。我们可以测试一下

    • 创建一个Animal类的子类CatAnimal类中有个实例变量name。不存在name属性。
    • 重写AnimalCat- (void)setValue:(id)value forKey:(NSString *)key方法。
    - (void)setValue:(id)value forKey:(NSString *)key {
        [super setValue:value forKey:key];
        NSLog(@"animal类中的value = %@", value);
    }
    
    • 我们先将Cat类中的super链打断
    - (void)setValue:(id)value forKey:(NSString *)key {
        NSLog(@"cat类中的value = %@",value);
      //  [super setValue:value forKey:key];
    }
    
    • 测试打印
     Cat *cat = [[Cat alloc] init];
    [cat setValue:@"小妖精" forKey:@"name"];
    NSLog(@"catName = %@",[cat valueForKey:@"name"]);
    
    • 结果cat赋值失败,因为cat中没有name实例变量
    NSKeyValueObserving[44918:845002] cat类中的value = 小妖精
    2018-07-02 10:10:49.221572+0800 KVO与NSKeyValueObserving[44918:845002] catName = (null)
    
    • 我们将cat中的super链连接.
    - (void)setValue:(id)value forKey:(NSString *)key {
        NSLog(@"cat类中的value = %@",value);
        [super setValue:value forKey:key];
    }
    

    打印结果,赋值成功,调用了父类中的- (void)setValue:(id)value forKey:(NSString *)key进行赋值,父类中有name实例变量。

    NSKeyValueObserving[44937:846921] cat类中的value = 小妖精
    2018-07-02 10:13:04.639062+0800 KVO与NSKeyValueObserving[44937:846921] animal类中的value = 小妖精
    2018-07-02 10:13:04.639263+0800 KVO与NSKeyValueObserving[44937:846921] catName = 小妖精
    

    KVO分别在KVC赋值的前后调用willChangeValueForKeydidChangeValueForKey实现监听调用- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context告诉我们值已经改变了。

    这样KVC调用的赋值,在我们实际开发中也有用到,类似TextField@"_placeholderLabel.font"这样的键值,我们在子类化TextField,创建subClassTextField这样一个子类后,仍然可以用KVC的方法通过键值对TextFieldplaceholderLabel实例变量进行赋值。

    查看KVC对实例变量调用KVO监听过程中,didChangeValueForKey的调用时机。

    • 设置系统自动触发KVO
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        return YES;
    }
    

    重写- (void)setValue:(id)value forKey:(NSString *)key方法

    - (void)setValue:(id)value forKey:(NSString *)key {
    // 查看当前isa指针指向的类
        Class runtimeClass = object_getClass(self);
        // 当前类的父类
        Class superClass = class_getSuperclass(runtimeClass);
        NSLog(@"runtimeClass = %@, superClass = %@",runtimeClass, superClass);
        NSLog(@"赋值前,name = %@",isName);
        [super setValue:value forKey:key];
        NSLog(@"赋值后,name = %@",isName);
    }
    
    • 输出结果
    2018-07-02 11:31:28.751652+0800 KVO与NSKeyValueObserving[45238:898392] runtimeClass = NSKVONotifying_Animal, superClass = Animal
    2018-07-02 11:31:28.752193+0800 KVO与NSKeyValueObserving[45238:898392] 赋值前,name = 牧尘
    2018-07-02 11:31:28.752513+0800 KVO与NSKeyValueObserving[45238:898392] 调用了observeValueForKeyPath方法
    2018-07-02 11:31:28.752667+0800 KVO与NSKeyValueObserving[45238:898392] 新值为小飞
    2018-07-02 11:31:28.752795+0800 KVO与NSKeyValueObserving[45238:898392] 旧值为牧尘
    2018-07-02 11:31:28.752940+0800 KVO与NSKeyValueObserving[45238:898392] 赋值后,name = 小飞
    

    我们可以看出,KVC调用实例属性触发didChangeValueForKey是在- (void)setValue:(id)value forKey:(NSString *)key方法中。

    我们可以看出KVO监听的过程实际上是对值的监听,在值改变的前后调用willChangeValueForKeydidChangeValueForKey,无论是点语法还是KVC都是直接或间接对值的改变。

    手动实现一个KVO

    写法思路都一样,推荐一个文章

    相关文章

      网友评论

        本文标题:KVO探究

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