美文网首页
KVO的相关知识(笔记)

KVO的相关知识(笔记)

作者: 我家冰箱养企鹅 | 来源:发表于2022-04-13 17:34 被阅读0次

    使用方法

    注册Observer

    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath 
    options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    

    options参数:
    NSKeyValueObservingOptionNew:接收方法的change参数的中包含更改后的值
    NSKeyValueObservingOptionOld:接收方法的change参数的中包含旧的值
    NSKeyValueObservingOptionInitial:注册的时候发一次通知,改变后也发送一次通知
    NSKeyValueObservingOptionPrior:属性改变之前发一次,改变之后再发一次

    触发方法

    • 直接调用setter方法,或者通过属性的点语法间接调用;
    • 使用KVC的setValue:forKey: 或者 setValue:forKeyPath:方法;
    • 通过mutableArrayValueForKey:K方法获取到数组代理对象,并使用代理对象进行操作(注意:直接增删是不会触发的,当数组属性添加了一个观察者之后,通过mutableArrayValueForKey获取的代理数组实际类型为NSKeyValueNotifyingMutableArray)
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
    change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
    
    1. change:在注册时用options参数进行的配置,会包含不同的内容,其中kind关键字的值含义如下
      kind = 1, 赋值 kind = 2, 插入kind= 3, 移除 kind = 4, 替换
      延伸
      I. kvo自定义结构体怎么取值?
      答:当监听属性是结构体时,可定义一个结构体类型并把它的地址传进去getValue方法里
      II. id是什么,和nsobject的区别?
      答:id是动态数据类型,而NSObject *是静态数据类型,默认情况下所有的数据类型都是静态。id类型的实例在编译阶段不会做类型检查,会在运行时确定,而类NSObject的实例在编译期要做编译检查,保证指针指向是其NSObject类或其子类,当然,实例的具体类型也要在运行时才能确定,这也就是iOS三大特性之一的多态。
      静态类型在编译时就知道变量的类型,编译时就知道变量的类型,在编译的时候就可以访问对象的属性和方法,如果访问了不属于静态类型的属性和方法,那么编译器就会报错,而动态数据类型在编译的时候并不知道变量的真实类型,只有在运行时的时候才知道它的真实类型,因此编译时候如果访问了不属于动态类型的属性和方法,编译器不会报错,导致运行时的错误,这也是动态数据类型的弊端。

    2. 实际上注册后,KVO默认会自动通知观察者,但其实也可以手动触发,首先在被观察的类中重写下面的方法:

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
    {
        if ( [key isEqualToString:@"value"] ) {
            return NO;
        }
        return YES;
    }
    

    在满足某些条件我们在触发调用接收方法,将条件代码包装下面任意一对方法中间。

    • (will/did)ChangeValueForKey:
    • (will/did)ChangeValueForKey:withSetMutation:usingObjects:
    • (will/did)Change:valuesAtIndexes:forKey:

    使用注意点

    1. 添加观察者几次,也要保证移除观察者几次
    [m1 addObserver:self forKeyPath:@"value" 
    options:NSKeyValueObservingOptionNew context:@""];
    [m1 addObserver:self forKeyPath:@"value" 
    options:NSKeyValueObservingOptionNew context:@""];
    

    如果重复给一个对象多次添加相同的观察者,那么当属性发生改变时也会多次调用接收方法,同时在被观察者被销毁前也要移除同样的次数,否则在低版本系统下会崩溃。
    原因在于iOS9之前NotificationCenter.default对self是unsafe_unretained引用,当self释放后,NotificationCenter.default持有的self并不会自动置为nil,而变成了一个野指针,这样再给self发送通知的话就会造成崩溃(给野指针发送消息, iOS9之后对于普通的添加观察者的方法不需要手动移除观察者self,因为iOS9之后NotificationCenter.default对self是weak引用,当self释放后,NotificationCenter.default持有的self会自动置为nil,而给一个nil发送推送的时候是不会发生崩溃的。

    1. 添加观察者和删除观察者以及各自的KeyPath要一一对应
    [m1 removeObserver:self.m2 forKeyPath:@"value" context:nil];
    
    [m1 addObserver:m3 forKeyPath:@"value" options:options context:@""];
    [m1 removeObserver:m2 forKeyPath:@"value" context:nil];
    
    [m1 addObserver:m2 forKeyPath:@"v" options:options context:@""];
    [m1 removeObserver:m2 forKeyPath:@"value" context:nil];
    

    以上几种情况程序都会崩溃。

    实现原理

    1. 创建MYObject,然后添加一个value属性;
    2. 创建两个MYObject类的实例对象,然后其中为m1对象添加观察者,然后观察两个对象的实际所属类
    self.m1 = [MYObject new];
    self.m1.value = 1;
        
    self.m2 = [MYObject new];
    self.m2.value = 2;
        
    NSLog(@"m1的类型:%@,m2的类型:%@",object_getClass(_m1),object_getClass(_m2));
    
    [_m1 addObserver:self forKeyPath:@"value" 
    options:NSKeyValueObservingOptionNewcontext:@""];
    
    NSLog(@"m1的类型:%@,m1的父类:%@, m2的类型:%@",object_getClass(_m1),
    class_getSuperclass(object_getClass(_m1)),object_getClass(_m2));
    
    打印结果

    从打印结果可以看到,m1对象未添加观察者前,m1和m2都是MYObject类的对象,添加之后,m1变成了NSKVONotifying_MYObject类的对象,并且它是MYObject的子类。
    下面观察m1和m2的setValue:方法分别做了什么,通过methodForSelector获取方法的地址,再用IMP把地址转化成实际的方法名:

    [_m1 methodForSelector:@selector(setValue:)]   0x10b826cb3
    [_m2 methodForSelector:@selector(setValue:)]   0x10a6b9d30
    
    方法名

    可以看到NSKVONotifying_MYObject类的setter方法是Foundation框架中的_NSSetIntValueAndNotify函数,它的内部实现过程大致如下:

    - (void)setValue:(int)value
    {
        _NSSetIntValueAndNotify();
    }
    
    void _NSSetIntValueAndNotify(){
        [self willChangeValueForKey:@"value"];
        [super setValue:value];
        [self didChangeValueForKey:@"value"];
    }
    
    - (void)didChangeValueForKey:(NSString *)key{
      [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
    }
    

    3.NSKVONotifying_MYObject类中的方法
    利用runtime打印一下类中的方法

    unsigned int outCount = 0;
    Method *methods = class_copyMethodList(object_getClass(self.m1), &outCount);
    for (int i = 0; i < outCount; ++i) {
        Method method = methods[i];
        NSString *name = NSStringFromSelector(method_getName(method));
        NSLog(@"%@", name);
    }
    free(methods);
    
    打印结果: image.png

    这四个方法的含义分别是:
    setValue: 实现值改变时的通知效果
    class:隐藏NSKVONotifying_MYObject类,直接返回他的父类。

    - (Class)class
    {
        return class_getSuperclass(object_getClass(self));
    }
    

    dealloc:runtime在实例对象添加了KVO之后动态创建了类和一些对象,所以可能会在dealloc中回收这些资源。
    _isKVOA: 是否使用了KVO。

    1. 注意点 - KVO对生成的中间类的格式是有要求的,默认都是以NSKVONotifying_<class>来命名,那如果我们不小心自己创建了一个一样名字的中间类,KVO就会无法使用。 image.png

    相关文章

      网友评论

          本文标题:KVO的相关知识(笔记)

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