KVC/KVO

过去的靠现在忘记,将来的靠现在努力,现在才最重要。

KVC

1. KVC概念

  KVC也就是key-value-coding,即键值编码,是一种间接访问实例变量的方法。提供一种机制来间接访问对象的属性。

2. KVC的主要用法

(1)通过键值路径为对象的属性赋值,主要是可以为私有的属性赋值。

  KVC也就是key-value-coding,即键值编码,通常是用来给某一个对象的属性进行赋值,例如有动物这么一个类,其对外有三个属性,类别、名字和年龄,我们在创建了一个动物animal后可以通过点语法直接给animal赋值。

// Animal.h文件
@interface Animal : NSObject

@property (nonatomic,strong)NSString *type;
@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)NSInteger age;

@end
  
// 创建对象并通过点语法赋值
Animal *animal = [[Animal alloc] init];
animal.type = @"dog";
animal.name = @"小白";
animal.age = 2;

  我们也可以通过KVC给这个动物animal赋值,代码如下,因为setValue这里的值是id类型的,所以将整数包装成一个对象:

// KVC赋值
[animal setValue:@"cat" forKey:@"type"];
[animal setValue:@"小花猫" forKey:@"name"];
[animal setValue:@3 forKey:@"age"];

  但是我们这样去赋值显得多此一举,可是如果人这个类的属性是没有暴露在外面呢?比如现在给动物这个类一个私有的体重的属性,并且对外提供一个输出体重的接口,如下:

// Animal.m文件
@implementation Animal {
    NSInteger _weight;
}

- (void)logWeight {
  NSLog(@"006 - KVC/KVO Weight = %ld",_weight);
}

@end
  
// 通过kvc直接对私有属性/变量进行赋值
[animal setValue:@15 forKey:@"weight"];
[animal logWeight];

// 输出结果
2021-08-22 21:36:03.040994+0800 001 - Class[2058:31772] 006 - KVC/KVO Weight = 15

  针对代码[animal setValue:@15 forKey:@"weight"];我们传入的字符串key是height,但是定义的属性是_height,但是通过KVC还是可以给_height属性赋到值。说明对某一个属性进行赋值,可以不用加下划线,而且它的查找规则应该是:先查找和直接写入的字符串相同的成员变量,如果找不到就找以下划线开头的成员变量。

  通过KVC对对象属性赋值的两种方法比较:

  除了[animal setValue:<#(nullable id)#> forKey:<#(nonnull NSString *)#>]这个方法外,还有一个方法也是可以对私有属性进行赋值的[animal setValue:<#(nullable id)#> forKeyPath:<#(nonnull NSString *)#>],这两个方法对于一个普通的属性是没有区别的,都可以用,但是对于一些特殊的属性就有区别了。比如说动物这个类有个属性是所属主人(动物的拥有者,也是用一个类来描述),而人又有属性体重。

animal.people = [[People alloc] init];
[animal setValue:@70 forKey:@"people.weight"];

  如果我们直接这样是会报错说找不到people.weight这个key的,而在storyboard中,我们拖控件连线错误的时候也会报错说找不到什么key,说明storyboard在赋值的时候也是通过KVC来操作的。

  这里如果我们换另外的一个方法,这时候是不会报错的,而且可以打印出人的体重。

[animal setValue:@70 forKeyPath:@"people.weight"];

// 输出结果
2021-08-22 22:01:03.944144+0800 001 - Class[6806:55872] 006 - KVC/KVO type=cat name=小花猫 age=3 people.weith=70

  说明forKeyPath是包含了forKey这个方法的功能的,甚至forKeyPath方法还有它自己的高级的功能,它会先去找有没有people这个key,然后去找有没有weight这个属性。所以我们在使用KVC的时候,最好用forKeyPath这个方法。

(2)通过键值路径获取属性的值,主要是可以通过key获得私有属性的值。

// 通过KVC进行取值
NSLog(@"006 - KVC/KVO name=%@", [animal valueForKey:@"name"]);
NSLog(@"006 - KVC/KVO people.weight=%@", [animal valueForKeyPath:@"people.weight"]);

(3)KVC的另外一个用处:字典转模型。

  KVC除了访问私有变量这个用处外,还可以用于字典转模型。在Animal类对外提供一个接口,将转模型的工作放在模型中进行。

- (instancetype)initWithDict:(NSDictionary *)dict {
    if (self = [super init]) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}

  外面可以直接将字典传入,和平常转模型相比,KVC更加方便,减少了代码量。

// 字典转模型
NSDictionary *animalDict = @{@"type":@"dog", @"name":@"小黑",@"age":@"3"};
Animal *animal2 = [[Animal alloc] initWithDict:animalDict];

  通过KVO字典转模型要注意以下几点:

  1. 字典转模型的时候,字典中的某一个key一定要在模型中有对应的属性,否则就会报错NSUnknownKeyException;当然我们可以实现下面这个函数来解决这个问题:

    - (void)setValue:(id)value forUndefinedKey:(NSString *)key{
      // 函数体可以为空    
      // 如果只是属性命名不一致,我们可以这样转换  
      if ([key isEqualToString:@"test"]) {        
          self.testID = value;    
      }
    }
    
  2. 如果一个模型中包含了另外的模型对象,是不能直接转化成功的;

(4)基于以上我们总结一下,KVC的基本用处:

  1. 通过键值路径为对象的属性赋值,主要是可以为私有的属性赋值;

  2. 通过键值路径获取属性的值,主要是可以通过key获得私有属性的值;

  3. 字典转模型;

  4. 修改一些控件的内部属性;

    例如设置:UITextField中的placeHolderText

    [textField setValue:[UIFont systemFontOfSize:25.0] forKeyPath:@"_placeholderLabel.font"];
    

    如何获取控件内部的属性:

    unsigned int count = 0;
    objc_property_t *properties = class_copyPropertyList([UITextField class], &count);
    for (int i = 0; i < count; i++) {    
        objc_property_t property = properties[i];    
        const char *name = property_getName(property);    
        NSLog(@"name:%s",name);
    }
    
  5. 高阶消息传递

      当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。

    NSArray *arr = @[@"hubert", @"andy", @"cydia"];
    NSArray *arrCap = [arr valueForKey:@"capitalizedString"];
    for (NSString *str  in arrCap) {    
        NSLog(@"%@",str);
    }
    
  6. KVC中的函数操作集合

    6.1)简单集合运算符:

    • @avg
    • @count
    • @max
    • @min
    • @sum
    @interface Book : NSObject
    @property (nonatomic,assign)  
    CGFloat price;
    @end
    
    NSArray* arrBooks = @[book1,book2,book3,book4];
    NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
    

    6.2)对象运算符

    • @distinctUnionOfObjects
    • @unionOfObjects
    // 获取所有Book的price组成的数组,并且去重
    NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
    

KVO

1. KVO概念

  KVO 是键值观察者(key-value-observing),是苹果提供的一套事件通知机制(也叫做观察者模式)。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,只针对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO

  KVONSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不是一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。

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

2. KVO的基本使用

  1. 给对象的属性注册观察者:

    // 注册观察者
    [animal2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    
  2. 在观察者中实现监听方法,observeValueForKeyPath: ofObject: change: context:(通过查阅文档可以知道,绝大多数对象都有这个方法,因为这个方法属于NSObject):

    /** 观察者监听的回调方法 
    @param keyPath 监听的keyPath 
    @param object 监听的对象 
    @param change 更改的字段内容 
    @param context 注册时传入的地址值 */
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<nskeyvaluechangekey,id> *)change context:(void *)context {    
        // 获取变化前后的值    
        NSLog(@"006 - KVO change=%@",change);
    }
    
  3. 移除观察者:

    - (void)dealloc {    
        [animal2 removeObserver:self forKeyPath:@"name"];
    }
    
  4. 调用:

    // 以下调用方式都可以触发KVO
    animal2.name = @"新名字";
    [animal2 setName:@"新名字1"];
    [animal2 setValue:@"新名字2" forKey:@"name"];
    [animal2 setValue:@"新名字3" forKeyPath:@"name"];
    
  5. 手动调用:

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

      下面以animal2name属性为例:

    5.1)禁用自动调用:

    // name不需要自动调用,name属性之外的自动调用
     + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
     BOOL automatic = NO;
     if ([key isEqualToString:@"name"]) {
         automatic = NO;
     } else {
         automatic = [super automaticallyNotifiesObserversForKey:key];
     }
     return automatic;
     }
     
     // 单独设置某个属性
     + (BOOL)automaticallyNotifiesObserversOfName {
     return NO;
     }
    

      针对每个属性,KVO都会生成一个+ (BOOL)automaticallyNotifiesObserversOfXXX方法,返回是否可以自动调用KVO。

      按上面实现上述方法,我们会发现,此时改变name属性的值,无法触发KVO,还需要实现手动调用才能触发KVO。

    5.2)手动调用实现:

    // KVO 手动调用实现
     - (void)setName:(NSString *)name {
         if (_name != name) {
             [self willChangeValueForKey:@"name"];
             _name = name;
             [self didChangeValueForKey:@"name"];
         }
     }
    

      实现了(1)禁用自动调用(2)手动调用实现 两步,name属性手动调用就实现了,此时能和自动调用一样,触发KVO。

  6. Crash

      KVO若使用不当,极容易引发Crash,下面总结使用过程中常见问题。

    • 观察者未实现监听方法;

    • 未及时移除观察者;

    • 多次移除观察者;

  7. keyPath字符串的弊端

      在注册Observe时,传入keyPath为字符串类型,keyPath极容易误写。

    // 注册观察者
    [animal2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    

      我们可以这样优化:

    [animal2 addObserver:self forKeyPath:NSStringFromSelector(@selector(name)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    
  8. 属性依赖

    // 属性依赖:如果属性type改变,观察者也能收到name改变的通知
    + (NSSet<nsstring *=""> *)keyPathsForValuesAffectingName {    
        NSSet *set = [NSSet setWithObjects:@"type", nil];    
        return set;
    }
    
  9. 监听集合对象的变化

    ​   首先,数组不能直接使用KVO使用监听。当我们想要使用KVO监听数组的状态时改变时,我们需要进行一下几部。

    9.1)KVO不能直接监听UIViewController中的数组变化。我们需要先创建一个模型,将需要监听的数组封装到模型中。然后控制器UIViewController持有模型对象,通过该对象才能监听。

    @interface ObserveArray : NSObject
    
    @property (nonatomic,strong)NSMutableArray *datas;
    
    - (id)initWithDict:(NSDictionary *)dict;
    
    @end
    

    9.2)建立观察者与观察的对象,在观察者中实现监听方法

    // ViewController.m 文件中
    @interface ViewController () 
      
    // 数组模型对象,来代替纯数据保存数据  
    @property (nonatomic,strong)ObserveArray *observeArray;
    // 临时数组,用来接收被观察的模型中的数据,方便数据操作
    @property (nonatomic,strong)NSMutableArray *tempArray;
    
    @end
      
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
      	// 创建观察的对象模型
      	NSDictionary *dict = [NSDictionary dictionaryWithObject:[NSMutableArray arrayWithCapacity:0] forKey:@"datas"];
        _observeArray = [[ObserveArray alloc] initWithDict:dict];  
      
      	[self KVOObserverArray];
    }
    
    // 给数组模型添加观察者
    - (void)KVOObserverArray {
        // 建立观察者以及观察者对象
        [_observeArray addObserver:self forKeyPath:@"datas" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
        
      	// 模拟改变数据,这里不能直接[_observeArray.datas addobject"@""];
        //[[_observeArray mutableArrayValueForKey:@"datas"] addObject:@"测试"];
        _tempArray = [_observeArray mutableArrayValueForKey:@"datas"];
        [_tempArray addObject:@"测试1"];
    }
    
    // 在观察者中实现监听方法
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        if ([keyPath isEqualToString:@"datas"]) {
            NSLog(@"006 - KVO Array change=%@",change);
        }
    }
    

    9.3)移除观察者

    - (void)dealloc {
        if (_observeArray != nil) {
            [_observeArray removeObserver:self forKeyPath:@"datas"];
        }
    }
    

3. KVO原理

  当一个类的对象属性被观察时,系统会通过runtime动态的创建一个该类的派生类,并且会在这个类中重写基类被观察的属性的setter方法,并且在setter方法中实现了通知的机制。派生类重写了class方法,以“欺骗”外部调用者他就是原先那个类。而且系统将这个类的isa指针指向新的派生类,因此改对象也就是改新的派生类的对象了。从而实现了给监听的属性赋值时调用的是派生类的setter方法。从而激活键值通知机制,重写的setter方法会在调用原setter方法前后,通知观察对象值得改变。此外派生类还重写了delloc方法来释放资源。

  以下图片来自iOS程序猿的图,有问题可以联系作者删除!!!

4. KVO常见使用场景

  1. 下拉刷新、下拉加载监听UIScrollView的contentoffsize;
  2. webview混排监听contentsize;
  3. 监听模型属性实时更新UI;
  4. 监听控制器frame改变,实现抽屉效果;
  5. 监听集合对象对象的变化;
posted @ 2021-08-25 17:36  背包の技术  阅读(728)  评论(0编辑  收藏  举报