KVC&KVO拾遗
关于KVC&KVO的不错的文章:
KVO
原生的KVO使用起来还是比较麻烦的,参考如何优雅地使用 KVO,推荐使用facebook/KVOController
在使用KVO时,一般会重写如下的方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
如果在这个方法中直接NSLog
输出change
,这个字典的结构可能如下:
{
kind = 1;
new = Michael;
old = George;
}
在这里kind
键对应的value
为1
,而如果对数组应用KVO,其kind值则可能为2
或者3
。
change
字典中NSKeyValueChangeKindKey
键对应的可能的值,如下:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
KVO实现细节
在Key-Value Observing Implementation Details中,官网介绍如下:
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
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.
大致意思是说:自动的KVO使用了一种叫做isa-swizzling
的技术。当observer被注册来观察一个对象的属性时,被观察对象的isa
指针则会被修改,指向了一个中间类,而不是原来真正的类。结果就是isa
指针并不会真实的反映实例的class
可以做如下的验证,在添加观察者之前和之后,输出class
名,如下:
NSLog(@"before : %s", object_getClassName(_eocFamily));
[_eocFamily addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"after : %s", object_getClassName(_eocFamily));
控制台输出结果为:
before : EOCFamily
after : NSKVONotifying_EOCFamily
可见class
发生了变化
KVO原理:利用运行时, 生成一个对象的子类,并生成子类对象,并替换原来对象的isa指针,并且重写了set方法
验证是否为子类,通过如下的findSubClass
方法,来找class
的子类:
NSLog(@"before : %s", object_getClassName(_eocFamily));
NSLog(@"before subclass : %@", [ViewController findSubClass:[_eocFamily class]]);
[_eocFamily addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"after : %s", object_getClassName(_eocFamily));
NSLog(@"after subclass : %@", [ViewController findSubClass:[_eocFamily class]]);
+ (NSArray*)findSubClass:(Class)defaultClass
{
int count = objc_getClassList(NULL, 0);
if (count <= 0) {
return [NSArray array];
}
NSMutableArray *output = [NSMutableArray arrayWithObject:defaultClass];
Class *classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i < count; i++) {
if (defaultClass == class_getSuperclass(classes[i])) {
[output addObject:classes[i]];
}
}
free(classes);
return output;
}
控制台输出结果为:
before : EOCFamily
before subclass : (
EOCFamily
)
after : NSKVONotifying_EOCFamily
after subclass : (
EOCFamily,
"NSKVONotifying_EOCFamily"
)
可见NSKVONotifying_EOCFamily
为EOCFamily
的子类
数组
如果观察某个的属性为数组,则向数组中添加或者删除元素时,是不会触发通知的,原因是KVO是通过set方法来触发
如下,向array中添加数据,并不会触发通知
[_eocFamily addObserver:self forKeyPath:@"eocAry" options:NSKeyValueObservingOptionNew context:nil];
[_eocFamily.eocAry addObject:@"one"];
但如果设置array的值,则会触发通知
[_eocFamily addObserver:self forKeyPath:@"eocAry" options:NSKeyValueObservingOptionNew context:nil];
_eocFamily.eocAry = [NSMutableArray array];
控制台输出如下:
{
kind = 1;
new = (
);
}
但如果使用如下的方法,在array中添加一条记录,则会触发通知
[[_eocFamily mutableArrayValueForKeyPath:@"eocAry"] addObject:@"one"];
//控制台输出为
{
indexes = "<_NSCachedIndexSet: 0x60800003f6a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
one
);
}
属性依赖
以下内容见KVC 与 KVO 拾遗补缺
我们还会遇到这样的情况,比如有一个属性,它的值是依赖于另外的属性,还是以 Person
类为例,添加一个 fullName
属性:
var fullName: String {
get {
return "\(lastName) \(firstName)"
}
}
这个属性值是通过 lastName
和 firstName
这两个属性的值生成的。所以当这两个属性改变的时候,也相当于 fullName
的值也改变了。
对于这样的属性关系,我们可以通过实现 keyPathsForValuesAffectingValueForKey
方法在实体类中声明属性依赖,以 fullName
属性为例:
class Person: NSObject {
override class func keyPathsForValuesAffectingValueForKey(key: String) -> Set<String> {
if key == "fullName" {
return Set<String>(arrayLiteral: "firstName","lastName")
} else {
return super.keyPathsForValuesAffectingValueForKey(key)
}
}
}
这样,我们声明了 fullName
属性依赖于两个其他属性 lastName
,firstName
。在 lastName
和 firstName
的属性值改变后,也会触发 fullName
属性改变的通知。
KVC
valueForKey
方法的总体规则,先找相关方法,再找相关变量
- 先是找相关方法,如果相关方法找不到
那么去判断
accessInstanceVariablesDirectly
(默认返回为YES
) 是否返回YES
- 如果是
NO
,直接执行KVC
的ValueForUndefinedKey
(系统抛出一个异常,未定义Key
) - 如果是
YES
(系统默认) 继续再去找相关变量
- 如果是
具体过程参考iOS KVC
这里的相关方法是指get<Key>
、<key>
(容器类方法 countOf<Key>
和 objectIn<Key>AtIndex
),例如key
为name
,则包括
- (NSString*)getName{
return @"getname 方法";
}
- (NSString*)name{
return @"name 方法";
}
其中getName
的优先级大于name
方法
相关变量指的是_<key>
、_is<Key>
、<key>
、 is<Key>
,其优先级顺序也是一样的。
例如key
为name
,则包括_name
、_isName
、name
、isName
- 基本类型转换成
NSNumber
setValue: forKey:
方法与valueForKey
方法类似,先在相关的方法(set<Key> set<IsKey>
),没有相关方法则判断accessInstanceVariablesDirectly
,如果为NO
,则调用setValue:(id)value forUndefinedKey:
1.如下的例子,定义一个类EOCObject
,其有一个属性name
。在初始化方法中给name
赋值
- (instancetype)init{
self = [super init];
if (self) {
_name = @"_name";
}
return self;
}
此时通过valueForKey:
方法来获取,其值为_name
_eocObject = [EOCObject new];
NSString *str = [_eocObject valueForKey:@"name"];
NSLog(@"%@", str); //_name
2.此时如果重写其getName
方法,通过valueForKey:@"name"
获取的值则为getname 方法
- (NSString*)getName{
return @"getname 方法";
}
3.现在类EOCObject
中没有任何的属性和实例变量,在EOCObject.m
中,添加如下的两个方法
- (NSInteger)countOfName
{
return 2;
}
- (id)objectInNameAtIndex:(NSInteger)index
{
if (index == 0) {
return @"one";
}
return @"two";
}
此时通过[_eocObject valueForKey:@"name"]
获取值,控制台输出为:
(
one,
two
)
4.取消name
属性,添加一个实例变量NSString *_name
,如下:
@interface EOCObject : NSObject{
NSString *_name;
}
//@property (nonatomic, strong)NSString *name;
@end
同样在init
方法中赋值为_name
,此时同样通过[_eocObject valueForKey:@"name"]
获取值,其值为_name
但此时如果我们,重写accessInstanceVariablesDirectly
方法,返回值为NO
+ (BOOL)accessInstanceVariablesDirectly{
return NO;
}
此时通过[_eocObject valueForKey:@"name"]
获取值,则会抛出异常
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<EOCObject 0x6080000161d0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'
所以利用这个特性,可以隐藏一些私有的变量
KVC的应用
1.设置UITextField
的Placeholder
的颜色,其它方式可以参考iPhone UITextField - Change placeholder text color
[_textField setValue:[UIColor redColor] forKeyPath:@"placeholderLabel.textColor"];
2.获取array的count
NSMutableArray *array = [NSMutableArray array];
[array addObject:@"one"];
NSLog(@"count %@", [array valueForKey:@"@count"]);//count 1
3.@max @min @sum
最大值、最小值和总值
Person *personOne = [Person new];
personOne.age = @"10";
Person *personTwo = [Person new];
personTwo.age = @"30";
Person *personThree = [Person new];
personThree.age = @"20";
[personAry addObject:personOne];
[personAry addObject:personTwo];
[personAry addObject:personThree];
NSLog(@"%@", [personAry valueForKeyPath:@"@max.age"]);//30
NSLog(@"%@", [personAry valueForKeyPath:@"@min.age"]);//10
NSLog(@"%@", [personAry valueForKeyPath:@"@sum.age"]);//60
KVC字典转模型
参考:
可把字典转模型
NSDictionary *dic = @{@"name":@"张三",@"sex":@"男",@"age":@"22"};
PersonModel *test=[[PersonModel alloc]init];
[test setValuesForKeysWithDictionary:dic];
等同于如下的遍历:
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) {
[item setValue:value forKey:key];
}];