姬長信(Redy)

重新认识KVC

KVC

键值编码是一种由NSKeyValueCoding非正式协议启用的机制,对象采用该机制提供对其属性的间接访问。当对象符合键值编码时,其属性可通过字符串参数通过简洁,统一的消息传递接口寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。

您通常使用访问器方法来访问对象的属性。get访问器(或getter)返回属性的值。set访问器(或setter)设置属性的值。在Objective-C中,您还可以直接访问属性的基础实例变量。以任何这些方式访问对象属性都很简单,但需要调用特定于属性的方法或变量名。随着属性列表的增长或变化,访问这些属性的代码也必须如此。相反,符合键值编码的对象提供了一个简单的消息传递接口,该接口在其所有属性中都是一致的。

键值编码是一个基本概念,是许多其他Cocoa技术的基础,例如键值观察,Cocoa绑定,核心数据和AppleScript能力。在某些情况下,键值编码还有助于简化代码。

Key-Value-Coding,译名键值编码,最常见的就是字典。在日常开中都或多或少使用过KVC,但有的时候还是对KVC的取值和写值的时候有些疑惑,基于探索官方文档的描述来解决这些疑问。

成员变量、实例变量、属性

在深入KVC的流程之前,必须很清晰明了的知道这三者,还有一些开发的人不清楚这三者。下面是一份示例代码:

@interface Person: NSObject{
@public
NSString *name; // 成员变量
int age;        // 成员变量
Person *father; // 实例变量
id pet;         // 可能是实例变量也可能不是
} // 括号里的都为成员变量
@property (nonatomic, copy) NSSting *birthday;  // 属性
@end
复制代码

上述的代码已经标识了这三者的示例,也许有人会对"括号里的都为成员变量"有疑问,但是这句话是没有问题的。理由是实例变量是成员变量的一种特殊类型。

由于历史原因,现在零星的可以看到一些这样的代码,Xcode编译器底层从GCC升级成了LLVM,LLVM会对属性自动生成setter和getter,如果属性没有匹配成员变量会自动生成一个带下划线的成员变量,@synthesize name = _name该语句会强制执行生成setter和getter方法,但不建议这样做。这就是为啥在项目中有时一个属性name,有的地方使用self.name进行赋值,有的使用_name。

KVC-赋值过程

  1. 先依次查询有没有相关的方法:set: _set: setIs: 找到直接进行调用赋值。
  2. 若没有相关方法时,会查看类方法accessInstanceVariablesDirectly是否为YES时进入下一步。否则进入步骤4
  3. 为YES时,可以直接访问成员变量的来进行赋值,依次寻找变量_ _is is。找到则直接赋值,否则进入下一步。
  4. 将会调用setValue:forUndefinedKey:方法进行抛出异常。可以自定义的实现为未找到的场景来避免抛出异常的行为。

KVC-取值过程

  1. 先依次寻找是否有相关成员变量:get,,is,或者_, 有则进入步骤4,否则下一步
  2. 查看类方法accessInstanceVariablesDirectly是否为YES,是则进入下一步,否则进入步骤5
  3. 依次寻找__is,,或者is成员变量是否有值,有则进入步骤4,否则进入步骤5
  4. 如果检索到的属性值是对象指针,则只返回结果。如果值是支持的标量类型NSNumber,则将其存储在NSNumber实例中并 返回该值。如果结果是NSNumber不支持的标量类型,则转换为NSValue对象并返回该对象。
  5. 将会调用setValue:forUndefinedKey:方法进行抛出异常。可以自定义的实现为未找到的场景来避免抛出异常的行为

KVC取值、赋值过程中依次寻找的依据

上面的过程是查看官方文档后并实操后得出的,肯定有人会问是按顺序的吗?答案是肯定的。setter和getter的时候我们通过打印来得出的结果。下面我们都写了会寻找的几种格式的成员变量,并且重写了setter和getter。

@interface OMPerson : NSObject{
    @public
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
    
}

@property (nonatomic, copy) NSString *subject;

@end
复制代码
- (void)setName:(NSString *)name{
    NSLog(@"%s",__func__);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s",__func__);
}
...
复制代码
- (NSString *)getName{
    NSLog(@"%s",__func__);
    return NSStringFromSelector(_cmd);
}
- (NSString *)name{
    NSLog(@"%s",__func__);
    return NSStringFromSelector(_cmd);
}
...
复制代码

如果你有时间写出这些代码去跑的时候就会发现是依顺序的,不是随机从哪几种类型都去找的,毕竟也是消耗内存的。

KVC-探索

  1. 上述赋值和取值过程都是在NSObject子类中时的步骤,如果对象是数组、可变数组、可变有序集、可变集时的步骤则不同,可以通过官方文档详细了解这个过程。

  2. 上述过程都提及accessInstanceVariablesDirectly这个类方法,默认为YES。当赋值为空时,系统会调用setNilValueForKey方法抛出NSInvalidArgumentException异常,如果是key为空时,系统会调用setValue:forUndefinedKey方法抛出NSUnknownKeyException异常。

  3. validateValue该方法的工作原理是判断是否实现了下述方法并自行决定返回布尔值来决定是否合法,默认返回YES。

    - (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError{
    
        if (*ioValue == nil || inKey == nil || inKey.length == 0) {
            NSLog(@"value 可能为nil  或者key为nil或者空值");
            return NO;
        }
      
        return YES;
    }
    复制代码
  4. 说下keyPath的事,正常我们使用都是可以直接使用第一个方法,但是有些时候属性可能是个对象,需要修改属性对象中的属性值就需要使用keyPath了,例如:[person setValue: @"边牧犬" forKeyPath: @"son.pet"]。也就是第二个方法。

- (void)setValue:(nullable id)value forKey:(NSString *)key;

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
复制代码

KVC在数组中的应用

1.数组平均值
NSNumber *average = [array valueForKeyPath:@"@avg.self"];
复制代码
2.数组个数
int count = [[array valueForKeyPath: @"count.self"] intValue];
复制代码
3.数组求和
int sum = [[array valueForKeyPath:@"@sum.self"] intValue];
复制代码
4.数组最大值
float maxValue = [[array valueForKeyPath:@"@max.self"] floatValue];
复制代码
5.数组最小值
@interface OMPerson : NSObject{
    @public
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
    
}

@property (nonatomic, copy) NSString *subject;

@end
复制代码

0

6.数组全部集合
@interface OMPerson : NSObject{
    @public
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
    
}

@property (nonatomic, copy) NSString *subject;

@end
复制代码

1

7.数组去重集合
@interface OMPerson : NSObject{
    @public
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
    
}

@property (nonatomic, copy) NSString *subject;

@end
复制代码

2

上面的self代表数组元素本身,开发中往往都是一个数组中的对象,想计算每个元素的某个属性的值,就可以把self换成身高height等等。

KVC的高阶应用

1.数组中各个字符串的长度
@interface OMPerson : NSObject{
    @public
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
    
}

@property (nonatomic, copy) NSString *subject;

@end
复制代码

3

2.数组中字符串的大小写
@interface OMPerson : NSObject{
    @public
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
    
}

@property (nonatomic, copy) NSString *subject;

@end
复制代码

4

。。。

例如上面的用法,还有很多,例如首字母大小写等等之类的,我们可以自行进行探索。

KVC的应用场景

@interface OMPerson : NSObject{
    @public
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
    
}

@property (nonatomic, copy) NSString *subject;

@end
复制代码

5

@interface OMPerson : NSObject{
    @public
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
    
}

@property (nonatomic, copy) NSString *subject;

@end
复制代码

6

我们从上面知道了一些属性的赋值可以通过KVC来进行,还可以方便的操作集合和一些数组中的处理,我们都知道YYModel相关的字典转模型相关的库依据代码就能把字典解析成Model,当你阅读过YYModel的源码你就知道底层使用基于Runtime的KVC来实现的。KVC提供了简单的字典转模型和模型转字典的API。简单就是以下几点:

  • 属性赋值
  • 操作集合
  • 字典转模型
  • 使用一些私有属性(例如修改搜索框中的xx颜色)