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字典转模型要注意以下几点:
-
字典转模型的时候,字典中的某一个key一定要在模型中有对应的属性,否则就会报错
NSUnknownKeyException
;当然我们可以实现下面这个函数来解决这个问题:- (void)setValue:(id)value forUndefinedKey:(NSString *)key{ // 函数体可以为空 // 如果只是属性命名不一致,我们可以这样转换 if ([key isEqualToString:@"test"]) { self.testID = value; } }
-
如果一个模型中包含了另外的模型对象,是不能直接转化成功的;
(4)基于以上我们总结一下,KVC的基本用处:
-
通过键值路径为对象的属性赋值,主要是可以为私有的属性赋值;
-
通过键值路径获取属性的值,主要是可以通过key获得私有属性的值;
-
字典转模型;
-
修改一些控件的内部属性;
例如设置: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); }
-
高阶消息传递
当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。
NSArray *arr = @[@"hubert", @"andy", @"cydia"]; NSArray *arrCap = [arr valueForKey:@"capitalizedString"]; for (NSString *str in arrCap) { NSLog(@"%@",str); }
-
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
。
KVO
和NSNotificationCenter
都是iOS
中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO
是一对一的,而不是一对多的。KVO
对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO
可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC
的mutableArrayValueForKey:
等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO
监听的方法。集合对象包含NSArray
和NSSet
。
2. KVO的基本使用
-
给对象的属性注册观察者:
// 注册观察者 [animal2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
-
在观察者中实现监听方法,
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); }
-
移除观察者:
- (void)dealloc { [animal2 removeObserver:self forKeyPath:@"name"]; }
-
调用:
// 以下调用方式都可以触发KVO animal2.name = @"新名字"; [animal2 setName:@"新名字1"]; [animal2 setValue:@"新名字2" forKey:@"name"]; [animal2 setValue:@"新名字3" forKeyPath:@"name"];
-
手动调用:
KVO在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自己实现KVO属性的调用,则可以通过KVO提供的方法进行调用。
下面以
animal2
的name
属性为例: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。 -
Crash
KVO若使用不当,极容易引发Crash,下面总结使用过程中常见问题。
-
观察者未实现监听方法;
-
未及时移除观察者;
-
多次移除观察者;
-
-
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];
-
属性依赖
// 属性依赖:如果属性type改变,观察者也能收到name改变的通知 + (NSSet<nsstring *=""> *)keyPathsForValuesAffectingName { NSSet *set = [NSSet setWithObjects:@"type", nil]; return set; }
-
监听集合对象的变化
首先,数组不能直接使用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常见使用场景
- 下拉刷新、下拉加载监听UIScrollView的contentoffsize;
- webview混排监听contentsize;
- 监听模型属性实时更新UI;
- 监听控制器frame改变,实现抽屉效果;
- 监听集合对象对象的变化;