iOS KVO原理分析

2021-09-16  本文已影响0人  冼同学

前言

上一篇文章学习了KVC的原理(键值编码),KVC是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该机制来提供对其属性的间接访问。而KVO的实现是基于KVC键值编码,以下我们进行探讨。

准备工作

键值观察编程指南

KVO协议定义

KVO全称是Key-value Observing,翻译过来就是:键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。

注意:要使用KVO,首先必须确保被观察对象符合KVO。一般情况下,如果您的对象继承自NSObject并且您以通常的方式创建属性,则您的对象及其属性将自动符合KVO

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
 - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

KVO的使用

监听选项option

监听选项是由枚举NSKeyValueObservingOptions定义的:

    typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
            NSKeyValueObservingOptionNew = 0x01,
            NSKeyValueObservingOptionOld = 0x02,
            NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
            NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
    }

注意:option会影响通知中,提供的更改字典的内容以及生成通知的方式

上下文指针Context

addObserver:forKeyPath:options:context:消息中的上下文指针包含任意数据,这些数据将在相应的更改通知中传递回观察者。您可以指定NULL并完全依赖键路径字符串来确定更改通知的来源,但这种方法可能会导致超类由于不同原因也在观察相同键路径的对象出现问题。

一下实现一个案例,LGStudent继承自LGPerson,同时对两个对象的name属行进行设置,通过添加上下文指针context,可以在接收通知的地方进行过滤。见下图:

案例分析

KVO使用技巧

- (void)removeObserver:(NSObject *)anObserver
            forKeyPath:(NSString *)keyPath

- (void)removeObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
               context:(void *)context

这两个方法会根据传入的参数(主要是keyPathcontext)来移除观察者。移除观察者可以避免监听回调的混乱,保持良好的代码质量

注意:如果observer没有监听keyPath属性,依然调用上面两个方法会抛出异常,见下图:

抛出异常
由以上案例可知,观察者的移除是必须确认观察者已经被注册了,这样子才能调用移除观察者的方法,如果我们没有移除观察者也会出现崩溃的情况,请往下看。
添加观察时,两个对象(即观察者对象及属性所属的对象)都不会被retain,然而在这些对象被释放后,相关的监听信息却还存在,KVO做的处理是直接让程序崩溃。其实苹果官网也给出了相关说明,见下图:
官方说明
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key

默认情况下,该方法返回YES,即表示默认可以对任何类中的所有属性进行监听,可以理解为自动监听。在这种模式下,当我们修改属性的值时,KVO会自动调用以下两个方法:

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

开发过程中,可能不需要对所有属性进行监听,只要求选择性的观察部分属性。此时+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法返回NO,那么就需要对属性进行手动监听。见下面代码:

// 自动监听开关-关闭
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

- (void)setName:(NSString *)name{
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

此时自动监听开关已经关闭,如果需要监听person对象的name属性的变化,就需要在setter方法中添加willChangeValueForKeydidChangeValueForKey方法,两个方法必须成对出现,否则无效

如果我们在开发过程中只是针对某几个属性进行手动接收通知,其他的不需要手动接收通知,那么我们可以精确的做到这个动作,通过+automaticallyNotifiesObserversForKey:方法可以设置对象中哪些属性需要手动处理,那么可以自动处理。见下图:

案例
    - (void)setNick:(NSString *)nick{
        if (nick != _nick){
            [self willChangeValueForKey:@"nick"];
            _nick = nick;
            [self didChangeValueForKey:@"nick"];
        }
    }

补充:如果我们在setter方法之外改变了实例变量(如_nick),且希望这种修改被观察者监听到,则需要像在setter方法里面做一样的处理。这也涉及到我们通常会遇到的一个问题,在类的内部,对于一个属性值,何时用属性(self.nick)访问,而何时用实例变量(_nick)访问。一般的建议是,在获取属性值时,可以用实例变量;在设置属性值时,尽量用setter方法,以保证属性的KVO特性。当然,性能也是一个考量,在设置值时,使用实例变量比使用属性设置值的性能高不少

    - (NSString *)fullName {
        return [[NSString alloc] initWithFormat:@"he's full name is :%@ , %@", self.name, self.nick];
    }

定义了这种依赖关系后,需要以某种方式告诉KVO,当我们的被依赖属性修改时,会发送fullName属性被修改的通知。此时,我们需要重写NSKeyValueObserving协议的keyPathsForValuesAffectingValueForKey:方法,这个方法返回的是一个集合对象,包含了哪些影响key指定的属性依赖的属性所对应的字符串。所以对于fullName属性,该方法的实现如下:

    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"fullName"]) {
            NSArray *affectingKeys = @[@"name", @"nick"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }

看看fullName监听效果:

fullName监听效果

补充:集合(Set)也有一套对应的方法来实现集合代理对象,包括无序集合有序集合;而字典则没有,对于字典属性的监听,还是只能作为一个整体来处理

如果我们想到手动控制集合属性消息的发送,则可以使用上面提到的几个方法,即:

    -willChange:valuesAtIndexes:forKey:
    -didChange:valuesAtIndexes:forKey:

    或

    -willChangeValueForKey:withSetMutation:usingObjects:
    -didChangeValueForKey:withSetMutation:usingObjects:

注意:先要把自动通知关闭,否则每次改变KVO都会被发送两次。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

第三个参数,通常称之为变化字典(Change Dictionary),它记录了被监听属性的变化情况。这个字典中包含的值,会根据我们在添加观察者时设置的options参数的不同而有所不同,它包含了属性被修改的一些信息。我们可以通过以下key来获取我们想要的信息:


typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;

/* 
Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

其中,NSKeyValueChangeKindKey的值取自于NSKeyValueChange,它的值是由以下枚举定义的:

enum {
     // 设置一个新值。被监听的属性可以是一个对象,也可以是一对一关系的属性或一对多关系的属性。
     NSKeyValueChangeSetting = 1,
     // 表示一个对象被插入到一对多关系的属性。
     NSKeyValueChangeInsertion = 2,
     // 表示一个对象被从一对多关系的属性中移除。
     NSKeyValueChangeRemoval = 3,
     // 表示一个对象在一对多的关系的属性中被替换
     NSKeyValueChangeReplacement = 4
 };
 typedef NSUInteger NSKeyValueChange;

KVO实现原理

在上面了解了NSKeyValueObserving所提供的功能后,我们再来看看KVO的实现机制,以便更深入地的理解KVOKVO没有开源,所以我们无法从源代码的层面来分析它的实现。那么我们还是先查看官方的描述:

官方说明
翻译过来:自动键值观察是使用一种称为isa-swizzling的技术实现的。isa指针指向维护调度表的对象的类。 该调度表主要包含指向类实现的方法的指针,以及其他数据。当观察者为对象的属性注册时,被观察对象的isa指针被修改,指向中间类而不是真正的类。 因此,isa指针的值不一定反映实例的实际类
所以我们就提出了几个疑问,这个isa指向的中间类是什么?kvo观察的是setter方法,setter方法做了什么,调用的又是谁的setter方法?移除监听后这个中间类是否销毁呢?带着疑问我们继续往下走。

那么这个中间类是何时创建的呢?我们在调用addObserver:forKeyPath:options:context:方法之前,获取NSKVONotifying_LGPerson这个类,发现这个类并不存在。见下图:

案例分析
说明这个类应该是通过runtime在运行时动态生成的。
    #pragma mark **- 遍历方法-ivar-property**
    - (void)printClassAllMethod:(Class)cls{
        unsigned int count = 0;
        Method *methodList = class_copyMethodList(cls, &count);
        for (int i = 0; i<count; i++) {
            Method method = methodList[i];
            SEL sel = method_getName(method);
            IMP imp = class_getMethodImplementation(cls, sel);
            NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
        }
        free(methodList);
    }

在调用addObserver:forKeyPath:options:context:方法之后,调用该辅助方法,查看NSKVONotifying_LGPerson类中有哪些功能。见下图:

查看子类的方法
发现中间类重写了父类的四个方法。分别是setNickNameclassdealloc_isKVOA

注意:在完成观察者的销毁之后,这个中间类依然存在并没有被销毁。(为下次使用做准备,性能的考虑,避免重复创建),请继续往下看。

中间类没有销毁

总结

Objective-C基于强大的run time机制来实现KVO。当第一次观察某个对象的属性时,run time会创建一个新的继承自这个对象的classsubclass。在这个新的subclass中,它会重写所有被观察的keysetter方法,然后将对象的isa指针指向新创建的class(这个指针告诉Objective-C运行时某个对象到底是什么类型的)。所以实例对象变成了新的子类的实例。完成以上操作后,通过调用setter方法进行相关属性的变化时,操作的就是这个中间的子类。但是底层依然会将对中间类操作的状态,同步到原对象中。在进行监听移除后,对象的isa回复到原来的类上,且中间类没有跟着被移除

上一篇 下一篇

猜你喜欢

热点阅读