美文网首页
IOS KVO详解

IOS KVO详解

作者: 你duck不必呀 | 来源:发表于2020-02-22 16:24 被阅读0次

    KVO(键值观察)键值观察是一种机制,它允许将其他对象的指定属性的更改通知给对象。

    重要提示: 为了了解键值观察,您必须首先了解键值编码。

    使用

    要使用KVO,首先必须确保观察到的对象符合KVO。通常,如果您的对象继承NSObject并以常规方式创建属性,则您的对象及其属性将自动符合KVO标准

    1. 注册观察者对象
    - (void)addObserver:(NSObject *)observer //观察者
             forKeyPath:(NSString *)keyPath //键路径
                options:(NSKeyValueObservingOptions)options //选项,新值或旧值
                context:(void *)context;
    

    参数说明:

    context 传递给观察者的任意数据,没有可以填NULL(void *是C的指针类型)

        [self.person addObserver:self
                      forKeyPath:@"name"
                         options:NSKeyValueObservingOptionNew
                         context:NULL];
    
    1. 在观察者内部实现以下方法,以接受更改通知消息。
    - (void)observeValueForKeyPath:(NSString *)keyPath //键路径
                          ofObject:(id)object //受观察属性的对象
                            change:(NSDictionary<NSKeyValueChangeKey, id> *)change 
                           context:(void *)context;
    

    参数context说明:

    上下文指针context包含任意数据,这些数据将在相应的更改通知中传递回观察者。您可以指定NULL并完全依赖键路径字符串来确定更改通知的来源,但是这种方法可能会给对象的父类带来问题,该对象的父类也出于不同的原因而观察相同的键路径。
    一种更安全,更可扩展的方法是使用上下文context确保您收到的通知是发给观察者的,而不是超类的。
    可以为每个观察到的键路径创建一个不同的上下文,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析

    如果同时添加了多个观察者,也可以通过这种方式实现而不用嵌套多层去判断具体是那个属性

    static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
    static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
    

    这样在observeValueForKeyPath的方法里通过context区分

    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context {
     
        if (context == PersonAccountBalanceContext) {
            // Do something with the balance…
        } else if (context == PersonAccountInterestRateContext) {
            // Do something with the interest rate…
        } else {
            // Any unrecognized context must belong to super
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                   context:context];
        }
    }
    
    
    1. 当观察者不再接收消息时,使用removeObserver:forKeyPath:该方法注销观察者。至少在观察者释放内存之前调用此方法。

    如果没有移除观察者,会造成野指针错误

    比如写在dealloc方法里

    - (void)dealloc
    {
        [self.person removeObserver:self forKeyPath:@"name"];
    }
    
    1. 自动和手动触发KVO
      在被观察的对象中,通过重写automaticallyNotifiesObserversForKey:方法返回YES自动触发,NO手动触发.也可以通过参数key指定自动观察的属性
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
        if ([key isEqualToString:@"account"]) {
            return YES;
        }
        return NO;
    }
    
    • 手动触发需要在在更改值之前调用willChangeValueForKey:和更改值之后调用didChangeValueForKey:方法,通常是写在setter方法中
    - (void)setAccount:(NSString *)account{
        if (account != _account) {
            [self willChangeValueForKey:@"account"];
            _account = account;
            [self didChangeValueForKey:@"account"];
        }
    }
    
    1. 可变数组

    KVO是建立在KVC基础之上的

    可变数组hobby

    @property(nonatomic)NSMutableArray *hobby;
    

    注册监听

    [self.person addObserver:self
                      forKeyPath:@"hobby"
                         options:NSKeyValueObservingOptionNew
                         context:NULL];
    

    这种方式没有使用KVC机制,自然也不会触发监听.

    [self.person.hobby addObject:@"1"];
    

    mutableArrayValueForKey :方法是KVC的方法,这样写就会触发监听

    [[self.person mutableArrayValueForKey:@"hobby"] addObject:@"1"];
    

    KVO原理探究

    自动键值观察是使用isa-swizzling的技术实现的。

    • 对象的isa指针指向对象的类,这个类中保存一个调度表。该分派表实质上包含指向该类实现的方法的指针以及其他数据。
    • 在为对象的属性注册观察者时,将修改对象的isa指针,指向中间类而不是真实类。(因此,isa指针的值不一定反映实例的实际类,不要依靠isa指针来确定类成员。相反,您应该使用该class方法确定对象实例的类。
    • 观察是属性的setter方法
    1. 动态生成子类(中间类),原对象的isa指向生成的中间类

    (1) 通过断点,查看注册前后isa的指向

    isa = (Class)Person 0x000000010326ab38
    

    注册后isa指向

    isa = (Class) NSKVONotifying_Person 0x000060000037ba80
    

    (2) 通过Runtime的API在注册观察者前后打印子类

     Class cls = [self.person class];
     int count = objc_getClassList(NULL, 0);
     // 创建一个数组, 用来存放类
     NSMutableArray *mArray = [NSMutableArray array];
        // 获取所有已注册的类
     Class* classes = (Class*)malloc(sizeof(Class)*count);
     objc_getClassList(classes, count);
     for (int i = 0; i<count; i++) {
         if (cls == class_getSuperclass(classes[i])) {
             [mArray addObject:classes[i]];
         }
      }
     free(classes);
     NSLog(@"classes = %@", mArray);
    

    注册观察者之前数组为空,注册之后多了一个子类,证明是动态产生的,并且是继承关系(中间类继承自实际类)

    "NSKVONotifying_Person"
    
    1. 重写setter方法

    动态生成的中间类,重写了父类的setter, class, dealloc 以及_isKVOA
    通过下面方式打印出两个类的所有方法:

    - (void)classAllMethod:(Class)cls{
        unsigned int count = 0;
        Method *methodList = class_copyMethodList(cls, &count);
        NSMutableArray *list = [NSMutableArray array];
        for (int i = 0; i<count; i++) {
            Method method = methodList[i];
            SEL sel = method_getName(method);
            IMP imp = class_getMethodImplementation(cls, sel);
            printf("%-16s--%p\n",[NSStringFromSelector(sel) UTF8String],imp);
        }
        free(methodList);
    }
    

    真实的类Person

    hobby           --0x10992da90
    setHobby:       --0x10992dab0
    init            --0x10992d8f0
    .cxx_destruct   --0x10992daf0
    name            --0x10992d9d0
    setName:        --0x10992d9f0
    setAccount:     --0x10992d840
    account         --0x10992da70
    age             --0x10992da30
    setAge:         --0x10992da50
    

    中间类NSKVONotifying_Person

    setHobby:       --0x109cb4c7a
    setAccount:     --0x109cb4c7a
    class           --0x109cb373d
    dealloc         --0x109cb34a2
    _isKVOA         --0x109cb349a
    

    比较中间类和真实类的方法IMP,证明子类是重写了被观察属性的setter方法;以及class方法,保证外界通过class方法返回的类是真实的Person;dealloc在移除观察者之后,isa指回真实Person类;之后动态生成的中间类会一直存在缓存中,不会销毁。

    3.移除的时候,对象的isa指回原来的类

    isa = (Class)Person 0x000000010326ab38
    

    总结一下原理就是:

    • 注册观察者时,系统会动态生成中间类(子类)
    • 对象isa指向中间类,该类重写了父类的setter, class, dealloc 以及_isKVOA
    • 移除观察者以后,对象isa又指回原来的类

    相关文章

      网友评论

          本文标题:IOS KVO详解

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