KVO原理分析

作者: 脚踏实地的小C | 来源:发表于2020-03-11 17:58 被阅读0次

  KVO(Key-Value Observing),是苹果提供的一套事件通知机制。允许对象监听特定属性的改变,并在改变时接收到事件。KVO 是建立在 KVC 上的,所以对属性才会发生作用,一般继承 NSObject 的对象都默认支持 KVO

一、KVO初探

1.1 context 的作用

//- (void)addObserver:(NSObject *)observer toObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

注意:context: 这里我们不应该用 nil,应该是用 NULL 才行,因为它对应的是 void *

@interface CHJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@property (nonatomic, strong) NSMutableArray *dateArray;
@property (nonatomic, strong) LGStudent *st;

@end

  我们可以看到,这里可以有多个对象,却有相同的keyPath,那么我们在方法 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 中该如何区分呢?

  通过 context 这种字符串直接匹配,会更加便利,更加安全,性能会更高。相当于一个 Tag

1.2 移除通知观察

  当我们不需要继续观察对象了,就需要移除观察者。在观察者被释放之前必须移除观察,否则观察者在释放后会再次接收到KVO,野指针会导致程序崩溃。

1.3 多嵌套路径

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

  这里我们以下载进度(downloadProgress = writtenData/totalData)为例,downloadProgresstotalData、writtenData相关,只要里面一个更改,对应的 downloadProgress 肯定也跟着发生变化。

1.4 自动和手动

  KVO默认是自动通知,也就是当我们属性的值变化时,就会自动发送通知。我们可以重写utomaticallyNotifiesObserversForKey:方法来控制是否启动自动通知。

  • 返回为YES时,该对象的所有属性启用自动通知。
  • 返回为NO时,该对象的所有属性禁用自动通知。

当我们要手动通知时,可以在 set<key> 中,手动调用 willChangeValueForKey:(更改值之前)和 didChangeValueForKey:(更改值之后)

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}
- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

1.5 可变数组

- (void)insertObject:(id)object inDateArrayAtIndex:(NSUInteger)index{
    [self.dateArray insertObject:object atIndex:index];
}

-(void)removeObjectFromDateArrayAtIndex:(NSUInteger)index{
    [self.dateArray removeObjectAtIndex:index];
}
    //------ 验证代码 ------
    // 数组变化
    [self.person.dateArray addObject:@"1"];
    // KVO 建立在 KVC
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];

打印信息如下:

2020-03-10 15:40:33.429762+0800 001---KVO初探[12157:813687] CHJViewController - {
    indexes = "<_NSCachedIndexSet: 0x6000030d9940>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 2;
    new =     (
        2
    );
}

  当我们直接调用addObject:时,是不走 insertObject: 方法,而通过 mutableArrayValueForKey: 是会走 insertObject: 方法的。

那么问题又来了,这个kind为什么会变成2呢?

/* Possible kinds of set mutation for use with -willChangeValueForKey:withSetMutation:usingObjects: and -didChangeValueForKey:withSetMutation:usingObjects:. 
Their semantics correspond exactly to NSMutableSet's -unionSet:, -minusSet:, -intersectSet:, and -setSet: method, respectively.
*/
typedef NS_ENUM(NSUInteger, NSKeyValueSetMutationKind) {
    NSKeyValueUnionSetMutation = 1, //正常的
    NSKeyValueMinusSetMutation = 2, //集合类型的
    NSKeyValueIntersectSetMutation = 3,//set的添加
    NSKeyValueSetSetMutation = 4//set其他情况
};

  由上可知,是因为NSKeyValueChangeKey是一个枚举,mutableArrayValueForKey: 时是集合类型。

二、KVO原理

  KVO的核心是 isa-swizzling。简单来说就是修改了原对象的 isa 指针,使其指向一个中间类而不是真正的类,所以 isa指针的值并不能反应实例的实际类,所以应该使用 class方法来确定对象的实际类。

@interface CHJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end

//----- 验证------
    self.person = [[CHJPerson alloc] init];
    Class cls = object_getClass(self.person);
    NSLog(@"cls == %@",NSStringFromClass(cls));
    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    Class cls2 = object_getClass(self.person);
    NSLog(@"cls2 == %@",NSStringFromClass(cls2));

打印信息如下:

2020-03-10 16:24:02.625080+0800 002---KVO原理探讨[14000:943952] cls == CHJPerson
2020-03-10 16:24:17.990241+0800 002---KVO原理探讨[14062:952265] cls2 == NSKVONotifying_CHJPerson

  我们发现两次输出的结果不一样,对象没有添加KVO之前,isa 指针指向的是 CHJPerson 类,添加以后,指向的是 NSKVONotifying_CHJPerson。至此我们可以得出对象在添加KVO之后,在运行时为我们动态生成了一个 NSKVONotifying_XXX 的中间类,并且将这个对象的 isa 指针指向了这个中间类。

2.1 这个 NSKVONotifying_CHJPersonCHJPerson是什么关系呢?

    Class cls2 = object_getClass(self.person);
    NSLog(@"cls2 == %@",NSStringFromClass(cls2));
    
    Class supCls = cls2;
    do {
        supCls = [supCls superclass];
        NSLog(@"supCls == %@",NSStringFromClass(supCls));
    } while (supCls);
    NSLog(@"supCls == %@",NSStringFromClass(supCls));

打印信息:

2020-03-10 16:47:05.437996+0800 002---KVO原理探讨[14494:1016365] cls2 == NSKVONotifying_ CHJPerson
2020-03-10 16:47:05.438197+0800 002---KVO原理探讨[14494:1016365] supCls == CHJPerson
2020-03-10 16:47:05.438286+0800 002---KVO原理探讨[14494:1016365] supCls == NSObject
2020-03-10 16:47:05.438378+0800 002---KVO原理探讨[14494:1016365] supCls == (null)
2020-03-10 16:47:05.438448+0800 002---KVO原理探讨[14494:1016365] supCls == (null)

  看到这,相信大家都晓得了吧,这个 NSKVONotifying_CHJPerson 是直接继承 CHJPerson的。

2.2 那么实例变量和属性的区别又是什么呢?

@interface CHJPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end

@implementation CHJPerson
- (void)setNickName:(NSString *)nickName{
    _nickName = nickName;
}
@end
  //-------- 验证 -------- 
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
    self.person.nickName = @"CHJ";
    self.person->name    = @"CC";
}

打印信息:

2020-03-10 17:00:35.855711+0800 002---KVO原理探讨[14803:1061366] 实际情况:(null)-(null)
2020-03-10 17:00:35.855979+0800 002---KVO原理探讨[14803:1061366] {
    kind = 1;
    new = CHJ;
}

  我们可以看到属性 name 没有被打印出来,而实例变量 nickName 却有。从而可以得出 KVO 主要观察的应该是 setter

2.3 CHJPerson 的动态子类里面做了些什么呢?

@interface CHJPerson : NSObject
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
- (void)sayGod;
@end

@interface CHJStudent : CHJPerson
@end
@implementation CHJPerson
- (void)sayHello{
}
//---------验证---------
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_CHJPerson")];
    [self printClassAllMethod:[CHJStudent class]];

打印信息:

2020-03-10 17:39:30.760719+0800 002---KVO原理探讨[15685:1202958] *********************
2020-03-10 17:39:30.760832+0800 002---KVO原理探讨[15685:1202958] setNickName:-0x7fff2564cec6
2020-03-10 17:39:30.760935+0800 002---KVO原理探讨[15685:1202958] class-0x7fff2564b989
2020-03-10 17:39:30.761006+0800 002---KVO原理探讨[15685:1202958] dealloc-0x7fff2564b6ee
2020-03-10 17:39:30.761092+0800 002---KVO原理探讨[15685:1202958] _isKVOA-0x7fff2564b6e6
2020-03-10 17:39:30.761200+0800 002---KVO原理探讨[15685:1202958] *********************
2020-03-10 17:39:30.761268+0800 002---KVO原理探讨[15685:1202958] sayHello-0x10d48b540

   根据打印,我们可以得出,动态子类重写了setNickName (setter)classdealloc_isKVOA
   这里我多打印一个 CHJStudent是为了更加直观验证只有被重写了的,才会被打印出来。

2.4 移除观察,isa 是否指回来?

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"nickName"];
}

通过lldb,打印信息:

(lldb) po object_getClass(self.person)
CHJPerson
(lldb) 

所以在移除观察后,isa是指回 CHJPerson的。

2.5 动态子类是否会销毁?

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self printClasses:[LGPerson class]];
}
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
    
    // 注册类的总数
    int count = objc_getClassList(NULL, 0);
    // 创建一个数组, 其中包含给定对象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 获取所有已注册的类
    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(@"CHJViewController:classes = %@", mArray);
}

打印信息:

2020-03-10 17:56:09.977303+0800 002---KVO原理探讨[15919:1242029] LGViewController:classes = (
    CHJPerson,
    CHJStudent,
    "NSKVONotifying_CHJPerson"
)

由此,我们可以知道,动态的子类是不会被销毁的

小结:

  1. 动态生成子类 : NSKVONotifying_xxx
  2. 观察的是 setter
  3. 动态子类重写了很多方法 setterclassdealloc_isKVOA
  4. 移除观察的时候 isa 指向回来
  5. 动态子类不会销毁

未完待续。。。

相关文章

  • iOS高级进阶之KVO

    KVO的原理 分析原理 使用 手动调用 自己实现KVO NSObject+KVOBlock.h NSObject+...

  • ios KVO原理和实现简易KVO

    一内容概述 本文主要分析kvo的底层原理,以及如何自己实现简易kvo。kvo的官方解释:Key-value obs...

  • iOS底层学习文章

    iOS黑魔法-Method Swizzling Objective-C 反射机制 KVC原理剖析 KVO原理分析及...

  • KVO原理分析

    概述 KVO全称KeyValueObserving,翻译成键值观察,是苹果提供的一套事件通知机制。允许对象监听另一...

  • KVO原理分析

      KVO(Key-Value Observing),是苹果提供的一套事件通知机制。允许对象监听特定属性的改变,并...

  • KVO原理分析

    KVO的使用 KVO使用的三部曲:添加观察者、接受回调、移除观察者;1、为什么要移除观察者呢?如果不移除会造成什么...

  • KVO原理分析

    一、KVO底层实现原理 示例代码: KVO 的实现过程实际上是利用了 OC 的 runtime 机制,当一个实例对...

  • KVO原理分析

    1、KVO简介 KVO官方简介[https://developer.apple.com/library/archi...

  • Key-Value Observing(kvo)二:自定义kvo

    一、自定义kvo 在上篇文章 kvo原理分析[https://www.jianshu.com/u/a569f590...

  • KVO的原理

    KVO基本原理: KVO深入原理: 适用于:

网友评论

    本文标题:KVO原理分析

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