美文网首页
Key-Value Observing(kvo)一:原理分析

Key-Value Observing(kvo)一:原理分析

作者: HotPotCat | 来源:发表于2021-08-02 15:09 被阅读0次

    一、kvo简介

    Key-Value Observing Programming Guide
    对于kvo使用分为3步:

    • 1.Registering as an Observer
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    

    observer:要添加的监听者对象,当监听的属性发生改变时会通知该对象,必须实现- observeValueForKeyPath:ofObject:change:context:方法,否则程序会抛出异常。
    keyPath:监听的属性,不能传nil
    options:指明通知发出的时机以及change中的键值。
    context:是一个可选的参数,可以传任何数据。

    ⚠️添加监听的方法addObserver:forKeyPath:options:context:并不会对监听和被监听的对象以及context做强引用,必须自己保证他们在监听过程中不被释放。

    • 2.Receiving Notification of a Change
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
    
    • 3.Removing an Object as an Observer
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    

    1.1 options

    typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
        NSKeyValueObservingOptionNew = 0x01,//更改前的值
        NSKeyValueObservingOptionOld = 0x02,//更改后的值
        NSKeyValueObservingOptionInitial = 0x04,//观察最初的值(在注册观察服务时会调用一次触发方法)
        NSKeyValueObservingOptionPrior  = 0x08 //分别在值修改前后触发方法(即一次修改有两次触发)
    };
    

    可以看到NSKeyValueObservingOptions4个枚举值,测试代码如下:

    self.obj = [HPObject alloc];
    self.obj.name = @"hp1";
    [self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    self.obj.name = @"hp2";
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"change:%@",change);
    }
    

    修改options参数输出如下:

    //NSKeyValueObservingOptionNew
    change:{
        kind = 1;
        new = hp2;
    }
    //NSKeyValueObservingOptionOld
    change:{
        kind = 1;
        old = hp1;
    }
    //NSKeyValueObservingOptionInitial
    change:{
        kind = 1;
    }
    change:{
        kind = 1;
    }
    //NSKeyValueObservingOptionPrior
    change:{
        kind = 1;
        notificationIsPrior = 1;
    }
     change:{
        kind = 1;
    }
    //NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior
    change:{
        kind = 1;
        new = hp1;
    }
    change:{
        kind = 1;
        notificationIsPrior = 1;
        old = hp1;
    }
    change:{
        kind = 1;
        new = hp2;
        old = hp1;
    }
    // 0
    change:{
        kind = 1;
    }
    

    NSKeyValueChangeKey定义如下:

    typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
    
    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;
    
    • NSKeyValueChangeKindKey:指明了变更的类型,一般情况下返回的都是1。集合中的元素被插入,删除,替换时返回2、3、4
      NSKeyValueChange定义如下:
    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting = 1,//普通类型设置
        NSKeyValueChangeInsertion = 2,//集合元素插入
        NSKeyValueChangeRemoval = 3,//集合元素移除
        NSKeyValueChangeReplacement = 4,//集合元素替换
    };
    
    • NSKeyValueChangeNewKey:改变后新值的key,如果是集合返回的数据是集合。
    • NSKeyValueChangeOldKey:改变前旧值的key,如果是集合返回的数据是集合。
    • NSKeyValueChangeIndexesKey:若果是集合类型,这个键的值是NSIndexSet对包含了增加,移除或者替换对象的index
    • NSKeyValueChangeNotificationIsPriorKey:NSKeyValueObservingOptionPrior调用前标记。

    综上:

    • NSKeyValueObservingOptionNew:指明change字典中应该包含改变后的新值。
    • NSKeyValueObservingOptionOld:指明change字典中应该包含改变前的旧值。
    • NSKeyValueObservingOptionInitial:注册后立马调用一次,这种通知只会发送一次。可以做一些一次性的工作。当同时指定new/old/initial的情况时,initial通知只包含new值。(实际上还是old值,因为是注册后立马调用,所以实际上对它来说是新值。任何情况下initial都不会包含old
    • NSKeyValueObservingOptionPrior:修改前后触发,会调用两次。修改前触发会包含notificationIsPrior字段。当同时指定new/old时,修改前会包含old,修改后会包含newold。(一般的通知发出时机都是在属性改变后,虽然change字典中包含了oldnew,但是通知还是在属性改变后才发出)。
    • 0:直接传递0,在每次调用的时候都返回包含kindchange。可以理解为默认实现。

    1.2 context

    这个参数最后会被传递到监听者的响应方法中,可以用来区分不同通知,也可以用来传值。
    对于多个keyPath的观察,需要在observeValueForKeyPath同时判断objectkeyPath,可以声明一个静态变量传递给context用来区分不同的通知提高代码的可读性:

    static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
    

    当然如果子类和父类都实现了对同一对象的同一属性的观察,并且父类和子类都可能对其进行设值,那么这个时候用context区分就很有用了。

    1.3 移除观察者

    官方文档说了在观察者dealloc的时候被观察者不会自动移除观察者,还是会继续给观察者发送消息。需要自己保证移除。
    比如某个页面监听了一个对象的属性,这个对象是从前一个页面传递进来的(本质上是对象不被释放)。在不移除观察的情况下,多次进入这个页面在属性变化的时候就发生了crash

    image.png
    根本原因是之前进入页面的时候观察者没有移除,导致发送消息的时候之前的observer不存在。

    kvo的使用三步曲要完整

    当然如果页面是个单例则不会崩溃,如果addObserver每次都调用则会进行多次回调。

    二、kvo初探

    2.1 kvo手动自动通知

    在被观察者中实现automaticallyNotifiesObserversForKey可以控制kvo是否自动通知:

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        NSLog(@"%s key:%@",__func__,key);
        return NO;
    }
    
    • 当返回NO的时候不会自动调用通知,当返回YES的时候会进行自动通知。
    • automaticallyNotifiesObserversForKey是在注册观察者的时候进行调用的。所以在中途通过开关配置是无效的(只在addObserver第一次调用的时候调用)。
      image.png

    willChangeValueForKey & didChangeValueForKey手动通知

    - (void)setName:(NSString *)name {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    }
    
    • 可以通过willChangeValueForKeydidChangeValueForKey进行手动通知。
    • 手动通知key可以自己写(key必须存在类中),所以这里可以做映射。将多个属性的变化映射到一个属性上。
    • 手动通知不受自动开关状态的影响。
    • 如果手动和自动同时开启,则两个都会发送通知。

    2.2 嵌套层次的监听

    keyPathsForValuesAffectingValueForKey(key - keys)
    比如下载文件:
    下载进度 = 已下载数据大小 / 总数据大小。总数据大小由于添加文件可能会发生变化。

    @interface HPObject : NSObject
    
    @property (nonatomic, copy) NSString *downloadProgress;
    @property (nonatomic, assign) double writtenData;
    @property (nonatomic, assign) double totalData;
    
    @end
    
    @implementation HPObject
    
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        NSLog(@"%s key:%@",__func__,key);
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"downloadProgress"]) {
            NSArray *affectingKeys = @[@"totalData", @"writtenData"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }
    
    - (NSString *)downloadProgress {
        if (self.writtenData == 0) {
            self.writtenData = 10;
        }
        if (self.totalData == 0) {
            self.totalData = 100;
        }
        return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
    }
    
    @end
    

    监听和调用:

    
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self.obj addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"change:%@",change);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.obj.writtenData += 10;
        self.obj.totalData += 1;
    }
    
    - (void)dealloc {
        [self.obj removeObserver:self forKeyPath:@"downloadProgress"];
        NSLog(@"dealloc");
    }
    
    • keyPathsForValuesAffectingValueForKey中对key进行了映射(只在addObserver第一次调用的时候调用)。
    • keyPathsForValuesAffectingValueForKey中会进行递归映射,也就是totalDatawrittenData也会去查找自身的依赖。
    +[HPObject keyPathsForValuesAffectingValueForKey:] key:downloadProgress
    +[HPObject keyPathsForValuesAffectingValueForKey:] key:writtenData
    +[HPObject keyPathsForValuesAffectingValueForKey:] key:writtenData
    +[HPObject keyPathsForValuesAffectingValueForKey:] key:totalData
    +[HPObject keyPathsForValuesAffectingValueForKey:] key:totalData
    
    • 这个时候通过touchesBegan中调用writtenDatatotalData就能监听到downloadProgress的变化了。writtenDatatotalData设置值的时候会调用到downloadProgress中(newValue 取值)。所以只要有任一一个变化都会调用到observeValueForKeyPath中。
    • 在首次(所有的第一次,不论页面是否重建与否,这里是与keyPathsForValuesAffectingValueForKey次数对应的)touchesBegan时,observeValueForKeyPath在上面的案例中会调用3次。
      image.png
      可以看到确实是系统内部直接多调用了一次。

    2.3 kvo对可变数组的观察

    self.obj.dateArray = [NSMutableArray array];
    [self.obj addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
    [self.obj.dateArray addObject:@(1)];
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"change:%@",change);
    }
    

    上面的案例dateArray添加元素并不能触发kvo,需要修改为:

    [[self.obj mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
    [[self.obj mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
    [[self.obj mutableArrayValueForKey:@"dateArray"] removeObject:@"1"];
    [self.obj mutableArrayValueForKey:@"dateArray"][0] = @"3";
    

    输出:

    change:{
        indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
        kind = 2;
        new =     (
            1
        );
    }
    change:{
        indexes = "<_NSCachedIndexSet: 0x600000a635c0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
        kind = 2;
        new =     (
            2
        );
    }
    change:{
        indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
        kind = 3;
    }
    change:{
        indexes = "<_NSCachedIndexSet: 0x600000a635a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
        kind = 4;
        new =     (
            3
        );
    }
    
    • 通过mutableArrayValueForKey获取dateArray再添加就能监听到了。
    • 这个时候kind变为了2、3、4就与前面介绍的NSKeyValueChange对应上了。

    ⚠️kvo监听集合元素变化,需要用到kvc的原理机制才能监听到变化。由于kvo底层也是由kvc实现的

    集合相关API如下:
    NSMutableArraymutableArrayValueForKeymutableArrayValueForKeyPath
    NSMutableSetmutableSetValueForKeymutableSetValueForKeyPath
    NSMutableOrderedSetmutableOrderedSetValueForKeymutableOrderedSetValueForKeyPath
    These methods provide the additional benefit of maintaining key-value observing compliance for the objects held in the collection object
    说明了集合类型的要特殊处理,具体可以参考kvc的说明:Accessing Collection Properties

    2.3.1 可变数组专属API

    当然除了上面对于集合类型的赋值通过kvc相关接口还可以通过数组专属API来完成。

    @property (nonatomic, strong) NSMutableArray <HPObject *>*array;
    
    self.array = [NSMutableArray array];
    HPObject *obj1 = [HPObject alloc];
    obj1.name = @"obj1";
    [self.array addObject:obj1];
    HPObject *obj2 = [HPObject alloc];
    obj2.name = @"obj2";
    [self.array addObject:obj2];
    
    [self.array addObserver:self toObjectsAtIndexes:[NSIndexSet indexSetWithIndex:1] forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    self.array[0].name = @"_obj0";
    self.array[1].name = @"_obj1";
    

    输出:

    change:{
        kind = 1;
        new = "_obj1";
    }
    

    这样当self.array[1].name发生变化的时候就监听到了。这里本质上就相当于是对obj1的监听。后续数组中替换了1位置的数组是监听不到的。

    HPObject *obj3 = [HPObject alloc];
    obj3.name = @"obj3";
    self.array[1] = obj3;
    self.array[1].name = @"_obj3";
    

    这样替换后监听不到。

    三、kvo原理分析

    Key-Value Observing Implementation Details
    根据官方文档可以看到使用了isa-swizzling技术。
    You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

    3.1 isa-swizzling验证

    直接在addObserver的调用处打个断点:

    image.png
    addObserverobjHPObject变成了NSKVONotifying_HPObject
    1. 那么NSKVONotifying_HPObject是什么时候生成的呢?
      重新运行,在addObserver之前验证NSKVONotifying_HPObject
    (lldb) p objc_getClass("NSKVONotifying_HPObject")
    (Class _Nullable) $0 = nil
    

    这样就意味着NSKVONotifying_HPObject是在addObserver的时候底层动态生成的。

    1. NSKVONotifying_HPObjectHPObject有什么关系呢?
    - (void)printClasses:(Class)cls {
        //注册类总个数
        int count = objc_getClassList(NULL, 0);
        //先将类本身放入数组中
        NSMutableArray *array = [NSMutableArray arrayWithObject:cls];
        //开辟空间
        Class *classes = (Class *)malloc(sizeof(Class)*count);
        //获取已经注册的类
        objc_getClassList(classes, count);
        for (int i = 0; i < count; i++) {
            //获取cls的子类,一层。
            if (cls == class_getSuperclass(classes[i])) {
                [array addObject:classes[i]];
            }
        }
        free(classes);
        NSLog(@"classes = %@",array);
    }
    

    上面这段代码是打印类以及它的子类(单层)。
    调用:

    [self printClasses:[HPObject class]];
    [self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self printClasses:[HPObject class]];
    [self printClasses:objc_getClass("NSKVONotifying_HPObject")];
    

    输出:

    classes = (
        HPObject,
        HPCat
    )
    classes = (
        HPObject,
        "NSKVONotifying_HPObject",
        HPCat
    )
    classes = (
        "NSKVONotifying_HPObject"
    )
    
    • NSKVONotifying_HPObject是在addObserver过程中底层动态添加的。
    • NSKVONotifying_HPObjectHPObject的子类。NSKVONotifying_HPObject本身没有子类。

    3.2 kvo 生成子类分析

    既然NSKVONotifying_HPObjectHPObject的子类,那么它都有什么内容呢?
    方法:

    - (void)printClassAllProtocol:(Class)cls {
        unsigned int count = 0;
        Protocol * __unsafe_unretained _Nonnull * _Nullable protocolList = class_copyProtocolList(cls, &count);
        for (int i = 0; i < count; i++) {
            Protocol *proto = protocolList[i];
            NSLog(@"%s",protocol_getName(proto));
        }
        free(protocolList);
    }
    

    输出:

    setName:-0x7fff207bbb57
    class-0x7fff207ba662
    dealloc-0x7fff207ba40b
    _isKVOA-0x7fff207ba403
    

    同理可以获取协议,属性以及成员变量:

    - (void)printClassAllProtocol:(Class)cls {
        unsigned int count = 0;
        Protocol * __unsafe_unretained _Nonnull * _Nullable protocolList = class_copyProtocolList(cls, &count);
        for (int i = 0; i < count; i++) {
            Protocol *proto = protocolList[i];
            NSLog(@"%s",protocol_getName(proto));
        }
        free(protocolList);
    }
    
    - (void)printClassAllProprerty:(Class)cls {
        unsigned int count = 0;
        objc_property_t *propertyList = class_copyPropertyList(cls, &count);
        for (int i = 0; i < count; i++) {
            objc_property_t property = propertyList[i];
            NSLog(@"%s-%s", property_getName(property), property_getAttributes(property));
        }
        free(propertyList);
    }
    
    - (void)printClassAllIvars:(Class)cls {
        unsigned int count = 0;
        Ivar *ivarList = class_copyIvarList(cls, &count);
        for (int i = 0; i < count; i++) {
            Ivar ivar = ivarList[i];
            NSLog(@"%s-%s",ivar_getName(ivar),ivar_getTypeEncoding(ivar));
        }
        free(ivarList);
    }
    

    没有输出任何内容。那么核心就在方法了。
    _isKVOA很好理解用来判断是否kvo生成的类,class标记类型。setName:是对父类namesetter方法进行了重写。dealloc中进行了isa重新指回。

    3.2.1 class

    addObserver后调用class输出:

    (lldb) p self.obj.class
    (Class) $0 = HPObject
    

    那么重写class就是为了返回原来的类的信息。不会返回kvo类自己的class信息。

    3.2.2 dealloc

    既然NSKVONotifying_HPObject是动态创建的,那么它销毁吗?
    deallocremoveObserver前后分别验证:

    image.png
    可以看到移除后isa指回了原来的类,也就是dealloc中进行了isa的指回。并且NSKVONotifying_HPObject类仍然存在。

    3.2.3 setter

    既然重写了setName:观察属性,那么成员变量能观察么?增加age成员变量:

    [self.obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self.obj addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
    self.obj->age = 18;
    

    当对age进行赋值并没有触发回调。那么就说明了对setter方法进行的监听。
    deallocremoveObserver后查看name的值:

    image.png
    那就说明在kvo生成的类中对name的修改影响到了原始类。
    name下个内存断点:
    (lldb) watchpoint set variable self->_obj->_name
    Watchpoint created: Watchpoint 1: addr = 0x60000129b260 size = 8 state = enabled type = w
        watchpoint spec = 'self->_obj->_name'
        new value: 0x0000000000000000
    

    在赋值的时候堆栈如下:

    * thread #1, queue = 'com.apple.main-thread', stop reason = watchpoint 1
      * frame #0: 0x00007fff2018b1e2 libobjc.A.dylib`objc_setProperty_nonatomic_copy + 44
        frame #1: 0x000000010afecd70 KVODemo`-[HPObject setName:](self=0x000060000129b250, _cmd="setName:", name=@"HP") at HPObject.h:19:39
        frame #2: 0x00007fff207c2749 Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 646
        frame #3: 0x00007fff207c300b Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 68
        frame #4: 0x00007fff207bbc64 Foundation`_NSSetObjectValueAndNotify + 269
        frame #5: 0x000000010afed248 KVODemo`-[HPDetailViewController viewDidLoad](self=0x00007fe291e0d890, _cmd="viewDidLoad") at HPDetailViewController.m:43:14
    

    调用逻辑如下:

    -[HPObject setName:]
    Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
    Foundation`-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
    Foundation`_NSSetObjectValueAndNotify
    

    _NSSetObjectValueAndNotify汇编调用主要如下:

    "willChangeValueForKey:"
    call   0x7fff2094ff0e 
    "didChangeValueForKey:"
    "_changeValueForKey:key:key:usingBlock:"
    

    _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:中有获取observers操作:

    _NSKeyValueObservationInfoGetObservances
    

    那么意味着在处理完所有事情后会进行通知。
    并且有NSKeyValueWillChangeNSKeyValueDidChange

    image.png

    继续在observeValueForKeyPath的回调中打个断点:

    image.png
    确认是在NSKeyValueNotifyObserver通知中进行的回调。

    总结(kvo原理)

    • addObserver动态生成子类NSKVONotifying_XXX
      • 重写class方法,返回父类class信息。父类isa指向子类。
    • 给动态子类添加setter方法(所有要观察的属性)。
    • 消息转发给父类。
      • setter会调用父类原来的方法进行赋值,完成后进行回调通知。
    • 移除observer的时候isa指回父类。动态生成的子类并不会销毁。

    相关文章

      网友评论

          本文标题:Key-Value Observing(kvo)一:原理分析

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