• [iOS]-KVO+KVC


    参考的博客:

    GNUstep KVC/KVO探索(二):KVO的内部实现此博客KVO原理讲解的非常到位
    iOS八股文(十九)KVC、KVO
    [iOS开发]KVO+KVC
    教你一行代码使用 KVO(Facebook 出品 FBKVOController 源码使用及解读)

    KVO

    什么是KVO

    KVO全称Key Value Observing,其是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。观察者模式

    由于KVO的实现机制,只针对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO

    KVO可以监听单个属性的变化,也可以监听集合对象的变化。集合对象包含NSArrayNSSet。通过KVCmutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。

    Key-Value Observing 可翻译成健值观察,是观察者模式再iOS开发中的具体体现,同时也是Object-C动态性的具体表现。在开发过程中,如果需要外部动态的获得对象的某个属性变化的时机以及变化前后的值,这时候就可以使用KVO来完成。显然KVO也属于信息传递的一种方式。

    KVO的基本使用

    主要分为三个步骤:

    • 通过addObserver:forKeyPath:options:context:方法注册观察者
      • observer:观察者,监听属性变化的对象。该对象必须必须实现observeValueForKeyPath:ofObject:change:context:方法。
      • keyPath:要观察的属性名称。要和属性声明的名称一致
      • options:回调方法里收到被观察的属性的旧值或新值,枚举类型,系统为我们提供了4个方法
        • NSKeyValueObservingOptionOld:change中会包含key变化之前的值old
        • NSKeyValueObservingOptionNew:change中会包含key变化之后的值new
        • NSKeyValueObservingOptionInitial:change中不包含key的值,会在kvo注册时候立即发通知
        • NSKeyValueObservingOptionPrior:会在值发生改变前发出一次通知,改变后通知依然发出,也就是每个change会有两个通知。值变化之前发送通知的 change 中包含notificationIsPrior = 1; 值发生变化之后的的通知 change 不包含上面提到的notificationIsPrior ,可以跟 willChange 手动通知搭配使用
        • 我们也可以中间以竖线来进行多种选择NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew这样change既有new又有old
    • 观察对象发生改变,回调方法observeValueForKeyPath:ofObject:change:context:
      • keyPath:被观察对象的属性
      • object:被观察的对象
      • change:字典类型,存放相关的值,根据options传入的枚举来返回新值旧值或者noticationlsPrior = 1
      • context:注册观察者时候context传入的值
    • 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除,我们需要在观察者消失之前进行处理,否则就crash

    具体的使用样例可以详见我之前的博客:[iOS]-KVO基础

    对于对已经注册的KVO观察者的dealloc来说还存在一些问题:

    在这里插入图片描述在这里插入图片描述
    也就是说,我们调用addObserver:selector:name:object:方法创建的观察者就没有必要自己写[xxx removeObserver:self forKeyPath:@"xxxx"];来删除观察者了。

    KVO使用注意事项

    但是在使用KVO的时候很容易引起crash,所以需要多多注意:

    1. keyPath 不能为空字符串
    2. 注意在适合的地方removeObersver,如果观察实例比被观察实例先释放,这时候改变观察属性,会产生崩溃。
    3. 没有添加,直接移除观察关系,也会产生崩溃

    注意这个移除观察者的方法:[xxx removeObserver:self forKeyPath:@"xxxx"];

    • observer:观察者
    • keyPath:被观察对象的属性

    手动调用KVO

    KVO没法实现对数组元素内部的监听,此时就需要我们手动调用KVO

    KVO在属性发生改变时的调用时自动的,如果想要手动控制这个调用时机,或想要自己实现KVO属性的调用,则可以通过KVO提供的方法进行调用。

    1. 如果想要手动调用或者自己实现KVO需要重写下面的方法。该方法返回YES表示允许系统自动调用KVONO表示不允许系统自动调用
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
        BOOL automatic = NO;
        if ([theKey isEqualToString:@"date"]) {
            automatic = NO;//对该key禁用系统自动通知,若要直接禁用该类的KVO则直接返回NO;
        }
        else {
            automatic = [super automaticallyNotifiesObserversForKey:theKey];
        }
        return automatic;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 另外我们需要重写setter方法:
    - (void)setDate:(NSString *)date {
        if (date != _date) {
            [self willChangeValueForKey:@"date"];
            _date = date;
            [self didChangeValueForKey:@"date"];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    不过一般情况下手动出发KVO没有什么必要,这样的话即使在没有注册监听者前调用setter方法为属性赋值的时候都会调用道KVO的响应事件,打印结果如下:
    请添加图片描述
    所以我们不采用这种方法,直接在注册监听者之后再对被监听的属性重新赋值的时候在赋值操作前后分别调用willChangeValueForKeydidChangeValueForKey,例子如下(以对数组进行监听为例):

    Apple.h文件中:
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Apple : NSObject
    
    @property (nonatomic, strong) NSMutableArray *arrayTest;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    Apple.m文件中:
    #import "Apple.h"
    
    @implementation Apple
    
    - (void)setDate:(NSString *)date {
        _date = date;
        NSLog(@"setDate:");
    }
    //由于数组的旧值不能被observeValueForKeyPath方法的change获取到,所以我们将打印旧值的操作移动到willChangeValueForKey中
    - (void)willChangeValueForKey:(NSString *)key{
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey");
        if ([key isEqual:@"arrayTest"]) {
            NSLog(@"old value is: %@", self.arrayTest);
        }
    }
    
    - (void)didChangeValueForKey:(NSString *)key{
        NSLog(@"didChangeValueForKey - begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey - end");
    }
    
    @end
    
    
    ViewController.h文件中:
    #import <UIKit/UIKit.h>
    #import "Apple.h"
    
    @interface ViewController : UIViewController
    
    @property (nonatomic, strong) Apple *apple;
    
    @end
    
    
    ViewController.m文件中:
    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        _apple = [[Apple alloc] init];
        self.apple.arrayTest = [[NSMutableArray alloc] init];
    
        [self.apple addObserver:self forKeyPath:@"arrayTest" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
            
        [self.apple willChangeValueForKey:@"arrayTest"];
        [self.apple.arrayTest addObject:@"First!"];
        [self.apple didChangeValueForKey:@"arrayTest"];
        
    }
    
    //当属性变化时会激发该监听方法
    - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        //打印监听结果
        if ([keyPath isEqual:@"arrayTest"]) {
            //NSLog(@"old value is: %@", [change objectForKey:@"old"]);
            NSLog(@"new value is: %@", [change objectForKey:@"new"]);
        }  
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    该例子的打印结果如下:
    请添加图片描述
    可以发现我们已经监听到数组的旧值和新值了。

    KVO本质

    KVO是基于runtime机制实现的 ,具体的KVO底层实现可以参考该博客:GNUstep KVC/KVO探索(二):KVO的内部实现

    在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指针指向中间类。并且将class方法重写,返回原类的class,例子如下:

    前提:我们创建一个继承自NSObject类的Apple类,其中定义了一个名为date的字符串属性,并引入其类的头文件:
    
    ViewController.h文件中:
    #import <UIKit/UIKit.h>
    #import "Apple.h"
    
    @interface ViewController : UIViewController
    //声明apple属性,因为KVO只能监听属性
    @property (nonatomic, strong) Apple *apple;
    
    @end
    
    ViewController.m文件中:
    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        _apple = [[Apple alloc] init];
        NSLog(@"类对象 -%@", object_getClass(self.apple));
        NSLog(@"方法实现 -%p", [self.apple methodForSelector:@selector(setDate:)]);
        NSLog(@"元类对象 -%@", object_getClass(object_getClass(self.apple)));
            
        //开启对apple属性的键值监听
        [self.apple addObserver:self forKeyPath:@"date" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
            
        NSLog(@"类对象 -%@", object_getClass(self.apple));
        NSLog(@"方法实现 -%p", [self.apple methodForSelector:@selector(setDate:)]);
        NSLog(@"元类对象 -%@", object_getClass(object_getClass(self.apple))); 
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    打印结果如下:
    请添加图片描述
    从打印结果的图中,我们可以清晰地看到:

    • apple指向的类对象和元类对象以及对应监听的属性的set方法都发生了改变
    • 添加KVO后,apple中的isa指向了NSKVONotifying_Apple类对象
    • 添加KVO后,setDate:的实现调用的是:Foundation中的_NSSetObjectValueAndNotify方法

    isa-swizzling(类指针交换):
    就是把当前某个实例对象的isa指针指向一个新建造的中间类,在这个新建造的中间类上面做hook方法或者别的事情,这样不会影响这个类的其他实例对象,仅仅影响当前的实例对象。
    下图对于这个类指针交换讲解的就非常生动:
    在这里插入图片描述

    NSKVONotifying_Apple内部实现

    
    - setName:最主要的重写方法,set值时调用通知函数
    - class:返回原来类的class
    - dealloc
    - _isKVOA判断这个类有没有被KVO动态生成子类
    
    - (void)setDate:(NSString *)date {
    
    }
    
    - (Class)class {
        return [Apple class];
    }
    
    - (void)dealloc {
        // 收尾工作
    }
    
    - (BOOL)_isKVOA {
        return YES;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    isa指向中间类之后如何调用方法:

    • 调用监听的属性的设置方法,例如:setDate:,都会先调用NSKVONotify_Apple对应的属性设置方法
    • 调用非监听属性的设置方法,如print方法,就会通过NSKVONotify_ApplesuperClass来找到Apple类对象,在调用其Apple类对象中的test方法

    为什么要重写class方法:

    • 如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_Apple,就会将该类暴露出来

    setter的实现不同

    截图中我们可以看到set方法的实现在调用KVO后变成调用_NSSetObjectValueAndNotify这样一个C函数
    我们不知道其本身是什么样,不过我们可以进行测试:

    Apple.h文件中:
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Apple : NSObject
    
    @property (nonatomic, copy) NSString *date;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    Apple.m文件里:
    #import "Apple.h"
    
    @implementation Apple
    
    - (void)setDate:(NSString *)date {
        _date = date;
        NSLog(@"setDate:");
    }
    
    - (void)willChangeValueForKey:(NSString *)key{
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey");
    }
    
    - (void)didChangeValueForKey:(NSString *)key{
        NSLog(@"didChangeValueForKey - begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey - end");
    }
    
    @end
    
    
    ViewController.h文件里:
    #import <UIKit/UIKit.h>
    #import "Apple.h"
    
    @interface ViewController : UIViewController
    
    @property (nonatomic, strong) Apple *apple;
    
    @end
    
    
    ViewController.m文件里:
    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        _apple = [[Apple alloc] init];
        self.apple.date = @"7Days!";
            
        [self.apple addObserver:self forKeyPath:@"date" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
            
        self.apple.date = @"25Days!";
        
        
    }
    
    //当属性变化时会激发该监听方法
    - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        //打印监听结果
        if ([keyPath isEqual:@"date"]) {
            NSLog(@"old value is: %@", [change objectForKey:@"old"]);
            NSLog(@"new value is: %@", [change objectForKey:@"new"]);
        }
        
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    要实现该测试样例必须要注意:

    1. 监听者必须实现observeValueForKeyPath:ofObject:change:context:方法
    2. Apple.m中实现的那三个方法是测试的关键

    该样例的打印结果如下:请添加图片描述
    我们发现:

    • 先调用willChangeValueForKey方法
    • 接着调用原来的setDate方法
    • 最后调用didChangeValueForKey方法,并且通知监听者属性值已经改变,然后监听者执行observeValueForKeyPath:ofObject:change:context:处理监听事务。

    KVO部分相关问题

    1. KVO的本质是什么?
    • 利用runtimeAPI动态生成一个子类,并让实例对象的isa指向这个全新的子类
    • 当修改实例变量对象的属性时候,在全新子类的set方法中会调用Foundation_NSSetXXXValueAndNotify函数
    • willChangeValueForKey
    • 调用原来的setter
    • didChangeValueForKey:内部会触发监听器的监听方法
    1. 手动触发KVO(详见上方讲解)
    2. 直接修改成员变量会触发KVO嘛?
      答案是不会

    接着我们总结一下KVO的应用场景:

    1. 需要接收动态变化的时候
    2. 例如在AVFounditon中获取AVPlayer的播放进度,播放状态,也需要使用KVO来观察。
    #pragma mark - 监听
    - (void)currentItemAddObserver {
        // 监控状态属性,注意AVPlayer也有一个status属性,通过监控它的status也可以获得播放状态
        [self.player.currentItem addObserver:self forKeyPath:@"status" options: (NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew) context:nil];
        // 监控缓冲加载情况属性
        [self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
        // 缓冲不足暂停了
        [self.player.currentItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
        // playbackLikelyToKeepUp
        [self.player.currentItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
        // rate
        [self.player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    最后总结一下KVO的实现原理

    1. addObserver:forKeyPath:options:context:context调用的时候,会自动生成并注册一个该对象(被观察的对象)对应类的子类,取名NSKVONotify_Class,并且将该对象的isa指针指向这个新的中间类。
    2. 在该子类内部实现4个方法-被观察属性的set方法class方法isKVOdelloc
    3. 最关键的是set方法中,先调用willChangeValueForKey,再给成员变量赋值,最后调用didChangeValueForKeywillChangeValueForKeydidChangeValueForKey需要成对出现才能生效,在didChangeValueForKey中会去调用观察者的observeValueForKeyPath: ofObject: 方法。
    4. 重写class方法,这样避免外部感知子类的存在,同时防止在一些使用isKindOfClass判断的时候出错。
    5. isKVO方法作为实现KVO功能的一个标识。
    6. delloc里面还原isa指针。

    到这里,我们已经知道了KVO的原理和其使用以及注意事项,人们对于KVO可谓是又爱又恨,因为其原理让我们在类之前通信增加了一种方式,但确实容易crash,针对这个问题,FaceBook的开发者的第三方库FBKVOController就巧妙的解决了这一问题,下面我们详细讲解一下FBKVOController第三方库。

    FBKVOController浅析

    首先介绍一下FBKVOController的用法:

    1. 使用前先利用cocoa向项目中下载FBKVOController第三方库,具体第三方库的下载流程详见:[iOS]-Masonry的使用博客中前面的讲解。
    2. 在文件中引入下方头文件:
    #import "FBKVOController.h"
    #import "NSObject+FBKVOController.h"
    
    • 1
    • 2

    接下来我们就可以开始正常的使用了

    使用例子如下:

    ViewController.h文件中:
    #import <UIKit/UIKit.h>
    #import "Apple.h"
    
    @interface ViewController : UIViewController
    
    @property (nonatomic, strong) Apple *apple;
    
    @end
    
    #import "ViewController.h"
    #import "objc/runtime.h"
    #import "FBKVOController.h"
    #import "NSObject+FBKVOController.h"
    @interface ViewController ()
    @property (nonatomic, strong) FBKVOController *kvoController;
    @end
    
    
    ViewController.m文件中:
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        NSLog(@"hello world!");
        
        //创建被观察的类
        _apple = [[Apple alloc] init];
        //初始化设置_apple的属性值
        self.apple.date = @"7Days!";
        self.apple.arrayTest = [[NSMutableArray alloc] init];
        
        //方法一:创建 FBKVOController 对象,并被 VC 强引用,否则出了当前作用域,就会被销毁
        FBKVOController *kvoController = [[FBKVOController alloc] initWithObserver:self];
        _kvoController = kvoController;
        
        //添加观察
        [kvoController observe:self.apple keyPath:@"date" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {     NSLog(@"我的旧名字是:%@", change[NSKeyValueChangeOldKey]);
                NSLog(@"我的新名字是:%@", change[NSKeyValueChangeNewKey]);
        }];
        
        
        //方法二:无需主动创建 FBKVOController 对象,self.KVOController 直接懒加载创建FBKVOController 对象
        //可以直接对某个对象的多个成员变量执行 KVO
        //------真正实现一行代码搞定 KVO------
        [self.KVOController observe:self.apple keyPaths:@[@"date", @"arrayTest"] options:  NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
    
            NSString *changedKeyPath = change[FBKVONotificationKeyPathKey];
    
            if ([changedKeyPath isEqualToString:@"date"]) {
                NSLog(@"修改了日期!");
            } else if ([changedKeyPath isEqualToString:@"arrayTest"]) {
                NSLog(@"修改了数组!");
            }
    
            NSLog(@"旧值是:%@", change[NSKeyValueChangeOldKey]);
            NSLog(@"新值是:%@", change[NSKeyValueChangeNewKey]);
        }];
    
        //修改被监听的值
        self.apple.date = @"31Days!"; 
    }
    
    @end
    
    
    Apple.h文件中:
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Apple : NSObject
    
    @property (nonatomic, copy) NSString *date;
    
    @property (nonatomic, strong) NSMutableArray *arrayTest;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    Apple.m文件中:
    #import "Apple.h"
    
    @implementation Apple
    
    - (void)setDate:(NSString *)date {
        _date = date;
        NSLog(@"setDate:");
    }
    
    - (void)willChangeValueForKey:(NSString *)key{
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey");
        if ([key isEqual:@"arrayTest"]) {
            NSLog(@"old value is: %@", self.arrayTest);
        }
    }
    
    - (void)didChangeValueForKey:(NSString *)key{
        NSLog(@"didChangeValueForKey - begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey - end");
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109

    可以看到上方有两种方法可以注册监听,而且都是非常简洁好用。

    接着我们就分析一下这个库的好用的原因:

    1. 统一观察者
      FBKVOController中有个单例[_FBKVOSharedController sharedController]来统一观察所有的对象,所有的观察回调也都先来到_FBKVOSharedController中的observeValueForKeyPath:ofObject: change:context:方法中,然后再派发给对应KVOControllerBlock或者Action(Seletor)
    + (instancetype)sharedController
    {
      static _FBKVOSharedController *_controller = nil;
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
        _controller = [[_FBKVOSharedController alloc] init];
      });
      return _controller;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath
                          ofObject:(nullable id)object
                            change:(nullable NSDictionary<NSString *, id> *)change
                           context:(nullable void *)context
    {
      NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
    
      _FBKVOInfo *info;
    
      {
        // lookup context in registered infos, taking out a strong reference only if it exists
        pthread_mutex_lock(&_mutex);
        info = [_infos member:(__bridge id)context];
        pthread_mutex_unlock(&_mutex);
      }
    
      if (nil != info) {
    
        // take strong reference to controller
        FBKVOController *controller = info->_controller;
        if (nil != controller) {
    
          // take strong reference to observer
          id observer = controller.observer;
          if (nil != observer) {
    
            // dispatch custom block or action, fall back to default action
            if (info->_block) {
              NSDictionary<NSString *, id> *changeWithKeyPath = change;
              // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
              if (keyPath) {
                NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
                [mChange addEntriesFromDictionary:change];
                changeWithKeyPath = [mChange copy];
              }
              info->_block(observer, object, changeWithKeyPath);
            } else if (info->_action) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
              [observer performSelector:info->_action withObject:change withObject:object];
    #pragma clang diagnostic pop
            } else {
              [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
            }
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    在添加观察者的时候,会把添加的信息生成一个_FBKVOInfo对象。

    @interface _FBKVOInfo : NSObject
    @end
    
    @implementation _FBKVOInfo
    {
    @public
      __weak FBKVOController *_controller;
      NSString *_keyPath;
      NSKeyValueObservingOptions _options;
      SEL _action;
      void *_context;
      FBKVONotificationBlock _block;
      _FBKVOInfoState _state;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 移除观察者时机 在使用FBKVOController添加观察者的时候会动态关联对象,该对象的类为FBKVOController,而在类释放的时候,会调用类的delloc方法,关联的对象也会走delloc方法,而在这个时候统一去移除观察者。
    - (void)dealloc
    {
      [self unobserveAll];
      pthread_mutex_destroy(&_lock);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    - (void)_unobserveAll
    {
      // lock
      pthread_mutex_lock(&_lock);
    
      NSMapTable *objectInfoMaps = [_objectInfosMap copy];
    
      // clear table and map
      [_objectInfosMap removeAllObjects];
    
      // unlock
      pthread_mutex_unlock(&_lock);
    
      _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];
    
      for (id object in objectInfoMaps) {
        // unobserve each registered object and infos
        NSSet *infos = [objectInfoMaps objectForKey:object];
        [shareController unobserve:object infos:infos];
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1. 防止重复添加
      在添加的时候会把_FBKVOInfo对象存起来,再次添加的时候去比较,如果存在,就不继续添加,在判断重复的时候重写了_FBKVOInfo中的hash方法,即keyPath重复就不继续添加。
    - (void)_observe:(id)object info:(_FBKVOInfo *)info
    {
      // lock
      pthread_mutex_lock(&_lock);
    
      NSMutableSet *infos = [_objectInfosMap objectForKey:object];
    
      // check for info existence
      _FBKVOInfo *existingInfo = [infos member:info];
      if (nil != existingInfo) {
        // observation info already exists; do not observe it again
    
        // unlock and return
        pthread_mutex_unlock(&_lock);
        //此处就是之前已经存在相应的_FBKVOInfo对象了,所以直接返回
        return;
      }
    
      // lazilly create set of infos
      if (nil == infos) {
        infos = [NSMutableSet set];
        [_objectInfosMap setObject:infos forKey:object];
      }
    
      // add info and oberve
      [infos addObject:info];
    
      // unlock prior to callout
      pthread_mutex_unlock(&_lock);
    
      [[_FBKVOSharedController sharedController] observe:object info:info];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    hash查找_FBKVOInfo对象和判断找到的对象是否与之前的_FBKVOInfo相等的方法如下:

    - (NSUInteger)hash
    {
      return [_keyPath hash];
    }
    
    - (BOOL)isEqual:(id)object
    {
      if (nil == object) {
        return NO;
      }
      if (self == object) {
        return YES;
      }
      if (![object isKindOfClass:[self class]]) {
        return NO;
      }
      return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    手动关闭/打开KVO

    在被观察的类中,重写automaticallyNotifiesObserversForKey:方法,对应被观察的key返回NO,这时候就会不再调用观察者的observeValueForKeyPath:ofObject: change:context:方法:

    /// 手动关闭KVO(具体实现的时候我们可以根据key来if else决定那些被监听的值需要手动监听,那些不需要自动监听,需要手动监听就返回NO,需要自动监听就返回YES)
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        return NO;
    }
    
    • 1
    • 2
    • 3
    • 4

    而在被观察的类中,成对的调用willChangeValueForKeydidChangeValueForKey,即使被观察的key没有发生变化也会手动的触发KVO的回调:

    //手动调用这个方法来查看我们监听的值的变化情况
    - (void)invokeKVO{
        [self willChangeValueForKey:@"name"];
        
        [self didChangeValueForKey:@"name"];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    KVC

    什么是KVC

    定义在NSKeyValueCoding.h中,是一个非正式的协议。KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量

    NSKeyValueCoding中提供了KVC通用的访问方法,分别是getter方法valueForKeysetter方法setValue:forKey,以及其衍生的keyPath方法,这两个方法是各个类通用的。并且由KVC提供默认的实现,我们也可以自己重写对应的方法来改变实现。

    基础操作

    KVC主要对三种类型进行操作,基础数据类型及常量、对象类型、集合类型。
    在使用KVC时,直接将属性名当作key,并设置value,即可对属性进行赋值:

        _apple = [[Apple alloc] init];
        //初始化设置_apple的属性值
        self.apple.date = @"7Days!";
        self.apple.arrayTest = [[NSMutableArray alloc] init];
        
        [self.apple setValue:@"14Days!" forKey:@"date"];
        NSLog(@"%@", self.apple.date);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    打印结果:
    请添加图片描述

    多级访问

    除了对当前对象的属性进行赋值外,还可以对其更深层的对象进行赋值。例如对当前对象的address属性的street属性进行赋值。

    KVC进行多级访问时,类似于属性调用一样用点语法进行访问即可[myInformation setValue:@"123" forKeyPath:@"address.street"];:

    实际代码例子如下:

        _apple = [[Apple alloc] init];
        
        self.apple.littleApple = [[sonOfApple alloc] init];
        
        [self.apple setValue:@66 forKeyPath:@"littleApple.flag"];
        NSLog(@"%@", self.apple.littleApple.flag);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    运行结果:
    请添加图片描述

    传参nil

    如果对非对象传递一个nil值,KVC会调用setNIlValueForKey方法
    我们可以重写这个方法来避免

    不重写setNIlValueForKey时:

        _apple = [[Apple alloc] init];
        [self.apple setValue:nil forKey:@"flagTest"];
    
    • 1
    • 2

    运行结果:程序崩溃
    请添加图片描述

    重写setNIlValueForKey后:

    首先在Apple.m文件中重写setNIlValueForKey方法为如下(默认的该方法接收到nil之后会打印日志报错):
    - (void) setNilValueForKey:(NSString *)key {
        return;
    }
    
    //再去执行该代码
    _apple = [[Apple alloc] init];
    [self.apple setValue:nil forKey:@"flagTest"];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行结果没有报错!

    处理非对象

    • setValue时,如果要赋值的对象是基本类型,需要将值封装成NSNumber或者NSValue类型
    • valueForKey时,返回的是id类型的对象,基本数据类型也会被封装成NSNumber或者NSValue

    valueForKey可以自动将值封装成对象,但是setValue:forKey:却不行。我们必须手动讲值类型转换成NSNumber/NSValue类型才能进行传递

    KVC获取值的过程

    setValue:forKey

    KVO里面,我们使用setValue:forKey这一部分会导致触发KVO监听的过程,KVC触发调用了willdid

    先用一张图展示一下setValue:forKey的过程:
    在这里插入图片描述

    • 程序回先通过setter方法对属性进行设置
    • 如果没有找到set方法,KVC机制会检查+(Bool)accessInstanceVariablesDirectly(直接访问实例变量)方法有没有返回YES(默认返回YES
      • 如果重写方法成了NO,调用-setValueForUndefinedKey:(为未定义项设置值)抛出异常
      • 返回YES就去找成员变量并直接赋值,按照_key_isKey,key,iskey的顺序找,没找到就抛出异常

    赋值顺序的例子如下:

    //接口成员变量定义
    @interface Apple : NSObject {
        @public
        int cnt;
        int isCnt;
        int _isCnt;
        int _cnt;
    }
    
    	//初始化和为成员变量赋值
    	_apple = [[Apple alloc] init];
        _apple -> cnt = 10;
        _apple -> _cnt = 11;
        _apple -> _isCnt = 12;
        _apple -> isCnt = 13;
        
        [_apple setValue:@15 forKey:@"cnt"];
        
        NSLog(@"%@", [_apple valueForKey:@"cnt"]);
        NSLog(@"%d %d %d %d", _apple -> cnt, _apple -> _cnt, _apple -> _isCnt, _apple -> isCnt);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    打印结果为:
    请添加图片描述
    我们可以看到,最后成员变量_cnt的值由11变成了15,符合我们刚才说的赋值顺序,就是先找_cnt去赋值,而不是先去找cnt赋值。

    valueForKey

    先上流程图参考:
    在这里插入图片描述

    • 先后顺序搜索getKeykeyisKey_getKey_key五个方法,若某一个方法被实现,取到的即是方法返回的值,后面的方法不再运行。如果是BOOL或者int等类型,会将其包装成一个NSNumber对象
    • 如果五个方法都没有,还是会访问accessInstanceVariablesDirectly方法有没有返回YES(该方法默认返回YES
      • 如果重写方法成了NO,抛异常
      • 返回YES就去找成员变量并取值,取值顺序为_key_isKeykeyisKey

    查找顺序的例子如下:

    //接口成员变量定义
    @interface Apple : NSObject {
        @public
        int cnt;
        int isCnt;
        int _isCnt;
        int _cnt;
    }
    
    	//初始化和为成员变量赋值
    	_apple = [[Apple alloc] init];
        _apple -> cnt = 10;
        _apple -> _cnt = 11;
        _apple -> _isCnt = 12;
        _apple -> isCnt = 13;
        
        NSLog(@"%@", [_apple valueForKey:@"cnt"]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    打印结果为:
    请添加图片描述
    我们可以看到11是成员变量_cnt的值,我们输入的key@"cnt",结果找到的是_cnt的值,这个也符合了我们上方说的查找结果。

    NSObject(NSKeyValueCoding)

    KVCApi都声明在NSObject的分类NSKeyValueCoding中,所以如果想使用KVC务必确认该对象是NSObject的子类。

    其中比较重要的API有:

    @property (class, readonly) BOOL accessInstanceVariablesDirectly;
    
    - (void)setValue:(nullable id)value forKey:(NSString *)key;
    
    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
    
    - (id)valueForKey:(NSString *)key;
    
    - (nullable id)valueForKeyPath:(NSString *)keyPath;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    key&keyPath的区别,如果某个属性是一个对象,需要设置该属性的某个属性的时候,就可以使用keyPath,一步到位来设置或者获取属性,关于这些的详细使用详见上方的讲解。

    KVC的使用场景

    1. crash防护,可以自定义valueForUndefinedKey:从而实现crash到控制台打印的友好处理方式
    2. jsonmodel(这个在接受网络请求来的数据的时候非常好用)
    3. KVO的实现
    4. 访问和修改私有变量(KVC的本质是操作方法列表以及在内存中查找实例变量。我们可以利用这个特性访问类的私有变量。同样如果不想让外界的类使用KVC的方法访问本类的成员变量,可以将accessInstanceVariablesDirectly属性设置为NO
    5. 修改一些控件的内部属性(很多UI控件都是由内部UI控件组合而成的,但是Apple(苹果官方)没有提供访问这些控件的API,这样我们就无法正常地访问和修改这些空间的样式。而KVC在大多数情况下可以解决这个问题)

    另外我们在除了利用KVC 动态地对单取值和设值之外,还可以进行多值操作:

    KVC可以根据给定的一组key,获取到一组value,并且以字典的形式返回,获取到字典后可以通过key从字典中获取到value

    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    
    • 1

    同样,也可以通过KVC进行批量赋值。在对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含keyvalue的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给相应对象的属性赋值(其实这里有点像jsonModel):

    - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
    
    • 1
    	NSDictionary *dic = @{@"name" : @"book", @"age" : @"66", @"sex" : @"male"};
    	//这个类里面定义了一些属性,这些属性名和传进来的字典的key一致且属性名数量大于等于字典中的key
        StudentModel *model = [[StudentModel alloc] init];
        
        [model setValuesForKeysWithDictionary:dic];
        NSLog(@"%@",model);
        NSDictionary *modelDic = [model dictionaryWithValuesForKeys:@[@"name", @"age", @"studentSex"]];
        NSLog(@"modelDic : %@", modelDic);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果model 属性和 dic 不匹配,可以重写方法 -(void)setValue:(id)value forUndefinedKey:(NSString *)key

    //比如我们需要赋值的属性是studentSex,而传进来的字典的相关对应的key为sex,我们可以在该方法里面将key为sex的value赋值给studentSex属性进行补救
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
        if([key isEqualToString:@"sex"]) {
            self.studentSex = (NSString *)value;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    以上就是本文对KVC的全部总结。

  • 相关阅读:
    项目风险管理的5大关键点,你做了几点?
    二维区间最值差
    电脑重装系统后鼠标动不了该怎么解决
    提升微服务稳定性与性能:深入剖析Netflix Hystrix框架
    java.time.TemporalAmount详解
    面试题:Java反射和new效率对比,差距有多大?
    画图带你彻底弄懂三级缓存和循环依赖的问题
    【Java入门】交换数组中两个元素的位置
    实验室储样瓶耐强酸强碱PFA材质试剂瓶适用新材料半导体
    CRM项目记录(四)
  • 原文地址:https://blog.csdn.net/m0_52192682/article/details/125990263