KVC&KVO拾遗

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键对应的value1,而如果对数组应用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_EOCFamilyEOCFamily的子类


数组

如果观察某个的属性为数组,则向数组中添加或者删除元素时,是不会触发通知的,原因是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)"

  }

}

这个属性值是通过 lastNamefirstName 这两个属性的值生成的。所以当这两个属性改变的时候,也相当于 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 属性依赖于两个其他属性 lastNamefirstName。在 lastNamefirstName 的属性值改变后,也会触发 fullName 属性改变的通知。

KVC

valueForKey方法的总体规则,先找相关方法,再找相关变量

  1. 先是找相关方法,如果相关方法找不到
  2. 那么去判断accessInstanceVariablesDirectly(默认返回为YES) 是否返回YES

    • 如果是NO,直接执行KVCValueForUndefinedKey(系统抛出一个异常,未定义Key
    • 如果是YES(系统默认) 继续再去找相关变量

具体过程参考iOS KVC

这里的相关方法是指get<Key><key>(容器类方法 countOf<Key>objectIn<Key>AtIndex),例如keyname,则包括

- (NSString*)getName{
    return @"getname 方法";
}

- (NSString*)name{
    return @"name 方法";
}

其中getName的优先级大于name方法

相关变量指的是_<key>_is<Key><key>is<Key>,其优先级顺序也是一样的。
例如keyname,则包括_name_isNamenameisName

  1. 基本类型转换成NSNumber
  2. 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.设置UITextFieldPlaceholder的颜色,其它方式可以参考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];

    }];
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值