KVC 简介
KVC全称是Key Value Coding(键值编码),是一个基于NSKeyValueCoding非正式协议实现的机制,它可以直接通过key值对对象的属性进行存取操作,而不需通过调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定。
KVC提供了一种间接访问属性方法或成员变量的机制,可以通过字符串来访问对象的的属性方法或成员变量。
在实现了访问器方法的类中,使用点语法和KVC访问对象其实差别不大,二者可以任意混用。但是没有访问器方法的类中,点语法无法使用,这时KVC就有优势了。
KVC和KVO都是基于OC的动态特性和Runtime机制的。
KVC 通用的访问方法
1.通用的访问方法:
getter方法:valueForKey:
setter方法:setValue:forKey:
2.衍生的keyPath方法,用来进行深层访问(key使用点语法),也可单层访问:
keyPath的setter方法:setValue: forKeyPath:
keyPath的getter方法:valueForKeyPath:
示例:
Address.h:
#import <Foundation/Foundation.h>
@interface Address : NSObject
@property (copy, nonatomic) NSString *city;
@property (copy, nonatomic) NSString *street;
@end
Person.h:
#import <Foundation/Foundation.h>
#import "Address.h"
@interface Person : NSObject
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger *sex;
@property (strong, nonatomic) NSNumber *age;
@property (strong, nonatomic) Address *address;
@end
- (void)viewDidLoad {
[super viewDidLoad];
Person *myself = [[Person alloc] init];
[myself setValue:@"xds" forKey:@"name"];
NSLog(@"-------name = %@",myself.name);
NSLog(@"-------name = %@",[myself valueForKey:@"name"]);
/**
keyPath的setter方法:setValue: forKeyPath:
keyPath的getter方法:valueForKeyPath:
keyPath为多级访问,使用点语法
*/
//注意,这里要想使用keypath对adress的属性进行赋值,必须先给myself赋一个Address对象
Address *myAddress = [[Address alloc] init];
[myself setValue:myAddress forKey:@"address"];
//KeyPath为多级访问
[myself setValue:@"rizhao" forKeyPath:@"address.city"];
NSLog(@"-------city = %@",myself.address.city);
NSLog(@"-------city = %@",[myself valueForKeyPath:@"address.city"]);
}
keypath
除了对当前对象的属性进行赋值外,还可以对其更“深层”的对象进行访问。
keypath可以访问到array数组中所有存储的对象的属性,前提是对象类型是一样的。
例如下面例子中,通过valueForKeyPath:将数组中所有对象的name属性值取出,并放入一个数组中返回。
NSArray *names = [array valueForKeyPath:@"name"];
KVC 的多值操作
批量取值操作
KVC还有更强大的功能,可以根据给定的一组key,获取到一组value,并且以字典的形式返回。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
批量赋值操作
同样,也可以通过KVC进行批量赋值。使用对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含key、value的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给User对象的属性赋值。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
示例
示例如下:
//批量赋值NSDictionary *dic = @{
@"name":@"xiaoMing",
@"sex":@1,
@"age":@12,
@"address":myAddress
};
[myself setValuesForKeysWithDictionary:dic];
//批量取值NSArray *keys = @[@"name",@"age",@"sex",@"address"];
NSDictionary *values = [myself dictionaryWithValuesForKeys:keys];
NSLog(@"%@",values);
输出:
2018-08-25 15:45:57.316650+0800 KVC[1145:231458] {
address = "<Address: 0x60400003cc40>";
age = "<null>";
name = xds;
sex = 0;
}
使用 KVC 进行字典转模型
可以使用setValuesForKeysWithDictionary: 进行字典转模型。
假如传过来的Jason数据如下所示,注意到age传过来的的key为Age,这样在字典转模型时会报错,因为这个Age在模型类里面并没有定义。
JSON数据:
{
@"name":@"xiaoMing",
@"sex":@1,
@"Age":@12
}
我们可以在Person里重写 setValue:(id)value forUndefinedKey: 方法,这个方法是针对碰到未定义的key时怎么办的方法。
#import "Person.h"
@implementation Person
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
if ([key isEqualToString:@"Age"]) {
[self setValue:value forKey:@"age"];
}
}
@end
注意点
Json里的字段数量和字段名字应该和model类所匹配,Json少了字段不会出现问题,但是多了或者是字段名不对,就会崩溃。
不需对基本数据类型做处理,例如int型转NSNumber,内部会自动作出处理
NSArray和NSDictionary等集合对象,value都不能是nil,否则会导致Crash
异常信息和异常处理
当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个NSUndefinedKeyException的异常,并且应用程序Crash。
我们可以重写下面两个方法,根据业务需求合理的处理KVC导致的异常:
在取值时,未有对应的key:
- (nullable id)valueForUndefinedKey:(NSString *)key;
在赋值时,未有对应的key:
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
当通过 KVC 给某个非对象的属性赋值为 nil 时,例如使用 setValue:forkey 给 int 或 float 类型属性赋为 nil 时,会抛出 NSInvalidArgumentException 的异常并崩溃。
我们可以重写下面的方法,进行处理:
- (void)setNilValueForKey:(NSString *)key;
示例:
当我们为Person类的NSInteger类型的sex属性赋nil时,会报错
[myself setValue:nil forKey:@"sex"];
Person.m 重写 setNilValueForKey: 方法
- (void)setNilValueForKey:(NSString *)key{
if ([key isEqualToString:@"sex"]) {
[self setValue:@1111 forKey:@"sex"];
}else{
[super setNilValueForKey:key];
}
}
集合属性操作
当我们要操作一个对象里的集合属性时(NSArray、NSSet等),我们可以通过KVC方法取到集合属性,然后通过集合属性操作集合中的元素。实际开发中最常用的方法,叫做间接操作。
直接操作要实现下面的方法,但通常不会用到:
有序集合对应方法如下:
-countOf<Key>//必须实现,对应于NSArray的基本方法count:2 -objectIn<Key>AtIndex:
-<key>AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
-get<Key>:range://不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:
-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>://可选的,如果在此类操作上有性能问题,就需要考虑实现之
无序集合对应方法如下:
-countOf<Key>//必须实现,对应于NSArray的基本方法count:
-objectIn<Key>AtIndex:
-<key>AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
-get<Key>:range://不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:
-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>://这两个都是可选的,如果在此类操作上有性能问题,就需要考虑实现之
使用 KVC 进行集合类的运算
KVC 提供的 valueForKeyPath: 方法非常强大,可以在keyPath中嵌套集合运算符对集合中的对象进行相关的运算,例如求一个数组中所有Person对象的age总和。集合对象主要指NSArray和NSSet,不包括NSDictionary。
集合运算符的格式
keyPathToCollection.@collentionOperator.keyPathToproperty
keyPathToCollection:Left key path,要操作的集合对象,若调用 valueForKeyPath: 方法的对象本来就是集合对象,则可以省略;
collentionOperator:Collection operator,集合操作符,一般以@开头;
keyPathToproperty:Right key path,要运算的属性。
//Address.h
@interface Address : NSObject
@property (copy, nonatomic) NSString *city;
@property (copy, nonatomic) NSString *street;
@property (strong, nonatomic) NSNumber *cityNumber;
@property (assign, nonatomic) NSInteger streetNumber;
@end
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray *array = [NSMutableArray array];
for ( int i = 0 ; i < 5 ; i++ ) {
Address *address = [[Address alloc] init];
NSString *cityStr = [NSString stringWithFormat:@"city-%d",i];
//批量赋值
NSDictionary *dic = @{
@"city":cityStr,
@"street":@"street",
@"cityNumber":@(i),
@"streetNumber":@101
};
[address setValuesForKeysWithDictionary:dic];
[array addObject:address];
}
//返回数组中保存的对象的属性,返回值为数组
NSArray *cityArray = [array valueForKeyPath:@"city"];
NSLog(@"%@",cityArray);
}
集合运算符的分类
集合运算符主要分为三类:
集合操作符:处理集合包含的对象,并根据操作符的不同返回不同的类型,返回值以NSNumber为主。
数组操作符:根据操作符的条件,将符合条件的对象包含在数组中返回。
嵌套操作符:处理集合对象中嵌套其他集合对象的情况,返回结果也是一个集合对象。
1. 集合操作符
集合操作符处理 NSArray和 NSSet 及其子类这样的集合对象,并根据不同的操作符返回不同类型的对象,返回值一般都是 NSNumber。
@avg 用来计算集合中 right keyPath 指定的属性的平均值。
//@avg:平均值
NSNumber *avg = [array valueForKeyPath:@"@avg.streetNumber"];
NSLog(@"%@",avg);
@count 用来计算集合中对象的数量。备注:@count 操作符比较特殊,它不需要写 right keyPath,即使写了也会被忽略。
//@count:集合里对象的数量
NSNumber *count = [array valueForKeyPath:@"@count"];
NSLog(@"%@",count);
@sum 用来计算集合中 right keyPath 指定的属性的总和。
//@sum:总和
NSNumber *sum = [array valueForKeyPath:@"@sum.streetNumber"];
NSLog(@"%@",sum);
@max 用来查找集合中 right keyPath 指定的属性的最大值。
//@max:最大值
NSNumber *max = [array valueForKeyPath:@"@max.cityNumber"];
NSLog(@"%@",max);
@min 用来查找集合中 right keyPath 指定的属性的最小值。
//@min:最小值
NSNumber *min = [array valueForKeyPath:@"@min.cityNumber"];
NSLog(@"%@",min);
备注:@max 和 @min 在进行判断时,都是通过调用 compare: 方法进行判断,所以可以通过重写该方法对判断过程进行控制。
2. 数组操作符(因为返回的是数组,所以叫数组操作符)
@unionOfObjects将集合中的所有对象的同一个属性放在数组中返回。这个和直接写属性好像没区别。
NSArray *city = [array valueForKeyPath:@"@unionOfObjects.city"];
NSLog(@"%@",city);
NSArray *cityArray = [array valueForKeyPath:@"city"];
NSLog(@"%@",cityArray);
@distinctUnionOfObjects将集合中对象的属性进行去重并返回。
NSArray *streetNumberArr = [array valueForKeyPath:@"@distinctUnionOfObjects.streetNumber"];
NSLog(@"%@",streetNumberArr);
3. 嵌套操作符
嵌套操作符是对集合里的集合进行操作,比如数组里面的数组。
NSMutableArray *array1 = [NSMutableArray array];
for ( int i = 0 ; i < 5 ; i++ ) {
Address *address = [[Address alloc] init];
NSString *cityStr = [NSString stringWithFormat:@"city-%d",i];
//批量赋值
NSDictionary *dic = @{
@"city":cityStr,
@"street":@"street",
@"cityNumber":@(i),
@"streetNumber":@101
};
[address setValuesForKeysWithDictionary:dic];
[array1 addObject:address];
}
NSMutableArray *array2 = [NSMutableArray array];
for ( int i = 3 ; i < 9 ; i++ ) {
Address *address = [[Address alloc] init];
NSString *cityStr = [NSString stringWithFormat:@"city-%d",i];
//批量赋值
NSDictionary *dic = @{
@"city":cityStr,
@"street":@"street",
@"cityNumber":@(i),
@"streetNumber":@101
};
[address setValuesForKeysWithDictionary:dic];
[array2 addObject:address];
}
NSArray *array = @[array1,array2];
@unionOfArrays 是用来操作集合内部的集合,将所有right keyPath对应的属性放在一个数组中返回。
NSArray *result = [array valueForKeyPath:@"@unionOfArrays.city"];
@distinctUnionOfArrays 是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个数组中,并进行排重。
NSArray *result = [array valueForKeyPath:@"@distinctUnionOfArrays.city"];
@distinctUnionOfSets 是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个set中,并进行排重。
NSSet *result = [array valueForKeyPath:@"@distinctUnionOfSets .city"];
4. 小技巧
如果你想对集合里装的对象直接进行操作,可以将 right keyPath 直接写为 self。比如集合里装的是NSNumber对象,如下:
NSArray *numbers = @[@1,@1,@3,@5];
NSNumber *sum = [numbers valueForKeyPath:@"@distinctUnionOfObjects.self"];
NSNumber *avg = [numbers valueForKeyPath:@"@avg.self"];
KVC对数值和结构体型属性的支持
KVC可以自动的将数值或结构体型的数据打包或解包成NSNumber或NSValue对象,以达到适配的目的。
举个例子,Person类有个NSInteger类型的age属性,如下:
// Person.m
#import "Person.h"
@interface Person ()
@property (nonatomic,assign) NSInteger age;
@end
@implementation Person
@end
// Person.m
#import "Person.h"
@interface Person ()
@property (nonatomic,assign) NSInteger age;
@end
@implementation Person
@end
1. 修改值
我们通过KVC技术使用如下方式设置age属性的值:
[[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
我们赋给age的是一个NSNumber对象,KVC会自动的将NSNumber对象转换成NSInteger对象,然后再调用相应的访问器方法设置age的值。
2. 获取值
同样,以如下方式获取age属性值:
NSNumber *age = [person valueForKey:@"age"];
这时,会以NSNumber的形式返回age的值。
3. 注意点
我们不能直接将基本数据类型通过KVC赋值,需要把数据转成NSNumber或NSValue类型传入。
可以使用NSNumber的数据类型有:
+ (NSNumber*)numberWithChar:(char)value;
+ (NSNumber*)numberWithUnsignedChar:(unsignedchar)value;
+ (NSNumber*)numberWithShort:(short)value;
+ (NSNumber*)numberWithUnsignedShort:(unsignedshort)value;
+ (NSNumber*)numberWithInt:(int)value;
+ (NSNumber*)numberWithUnsignedInt:(unsignedint)value;
+ (NSNumber*)numberWithLong:(long)value;
+ (NSNumber*)numberWithUnsignedLong:(unsignedlong)value;
+ (NSNumber*)numberWithLongLong:(longlong)value;
+ (NSNumber*)numberWithUnsignedLongLong:(unsignedlonglong)value;
+ (NSNumber*)numberWithFloat:(float)value;
+ (NSNumber*)numberWithDouble:(double)value;
+ (NSNumber*)numberWithBool:(BOOL)value;
+ (NSNumber*)numberWithInteger:(NSInteger)valueNS_AVAILABLE(10_5,2_0);
+ (NSNumber*)numberWithUnsignedInteger:(NSUInteger)valueNS_AVAILABLE(10_5,2_0);
可以使用NSValue的数据类型有:
+ (NSValue*)valueWithCGPoint:(CGPoint)point;
+ (NSValue*)valueWithCGSize:(CGSize)size;
+ (NSValue*)valueWithCGRect:(CGRect)rect;
+ (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform;
+ (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
+ (NSValue*)valueWithUIOffset:(UIOffset)insets;
NSValue主要用于处理结构体型的数据,任何结构体都是可以转化成NSValue对象的,包括其它自定义的结构体。
属性验证
在调用 KVC 前可以先进行验证 key(keyPath) 和 value 的正确性,验证通过下面两个方法进行。验证方法默认实现返回 YES,可以通过重写对应的方法修改验证逻辑。
注意:验证方法需要我们手动调用,并不会在进行 KVC 的过程中自动调用。
//key方法
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//keyPath方法
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue
forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
主动去调用验证方法:
Person *person = [[Person alloc] init];
NSError *error;
NSString *name = @"xds";
if (![person validateValue:&name forKey:@"name" error:&error]) {
NSLog(@"%@", error);
}
单独验证
KVC还支持对单独属性做验证,可以通过定义validate<Key>:error:格式的方法,并在方法内部实现验证代码。在编写KVC验证代码的时候,应该先查找属性有没有自定义validate方法,然后再查找validateValue:方法,如果有则调用自己实现的方法,如果两个方法都没有实现则默认返回YES。
- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
if ((*ioValue ** nil) || ([(NSString *)*ioValue length] < 2)) {
if (outError != NULL) {
*outError = [NSError errorWithDomain:PersonErrorDomain
code:PersonInvalidNameCode
userInfo:@{ NSLocalizedDescriptionKey
: @"Name too short" }];
}
return NO;
}
return YES;
}
注意:这里 validateName 是 validate + 属性名称 组合而来的。
KVC 的搜索规则
在学习KVC的搜索规则前,要先弄明白一个属性的作用,这个属性在搜索过程中起到很重要的作用。这个属性表示是否允许读取实例变量的值,如果为YES则在KVC查找的过程中,从内存中读取属性实例变量的值。
@property (class, readonly) BOOL accessInstanceVariablesDirectly;
在KVC的实现中,依赖setter和getter的方法实现,所以方法命名应该符合苹果要求的规范,否则会导致KVC失败。
基础Getter搜索模式(valueForKey:/valueForKeyPath:)
1.首先按get<Key>、<key>、is<Key>的顺序查找getter方法,找到直接调用。
若方法的返回结果类型为是一个对象指针,则直接返回结果;
若类型为能够转化为NSNumber的基本数据类型,转换为NSNumber后返回;
否则,转换为NSValue返回。
2.上面的getter没有找到,查找countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndexes格式的方法。
如果countOf<Key>和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的集合代理(collection proxy object)。发送给这个代理集合(collection proxy object)的NSArray消息方法,就会以countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndexes这几个方法组合的形式调用。如果receiver的类实现了get<Key>:range:方法,给方法也会用于性能优化。
3.还没查到,那么查找countOf<Key>、enumeratorOf<Key>、memberOf<Key>:格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的集合代理(collection proxy object)。发送给这个代理集合(collection proxy object)的NSSet消息方法,就会以countOf<Key>、enumeratorOf<Key>、memberOf<Key>:组合的形式调用。
4.还是没查到,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_<key>,_is<Key>,<key>,is<Key>(注意大小写)的顺序直接搜索实例变量。如果搜索到了,则返回receiver相应实例变量的值。返回结果的处理见步骤1。
5.再没查到,调用valueForUndefinedKey:方法,报出异常。
总结一下:
先找相应的 getter 方法(get<Key>, <key>, is<Key>, 或者 _<key>),找到了则返回(对象类型直接返回,其它类型进行转换);
没有找到,则寻找 NSArray 相应的方法;
没有找到,则寻找 NSSet 相应的方法;
如果还没有,且 accessInstanceVariablesDirectly 类属性返回的是 YES,则去搜索实例变量(_<key>、_is<Key>、<key>、is<Key>)。如果发现了,则返回;
还没有,则转到 valueForUndefinedKey: 方法并抛出异常。
基础Setter搜索模式
这是setValue:forKey:的默认实现,给定输入参数value和key。试图在接收调用对象的内部,设置属性名为key的value,通过下面的步骤:
查找set<Key>:或_set<Key>命名的setter,按照这个顺序,如果找到的话,调用这个方法并将值传进去(根据需要进行对象转换)。
如果没有发现一个简单的setter,但是 accessInstanceVariablesDirectly 类属性返回YES,则查找一个命名规则为_<key>、_is<Key>、<key>、is<Key>的实例变量。根据这个顺序,如果发现则将value赋值给实例变量。
如果没有发现setter或实例变量,则调用setValue:forUndefinedKey:方法,并默认提出一个异常,但是一个NSObject的子类可以提出合适的行为。
总结:
先找 setter 方法(set<Key>:或_set<Key>);
没找到,如果则 accessInstanceVariablesDirectly 类属性返回的是 YES,则去查找实例变量(_<key>、_is<Key>、<key>、is<Key>),若找到,则赋值;
没有找到 setter 方法和实例变量,则转到 setValue:forUndefinedKey: 方法,并抛出异常。
KVC性能
根据上面KVC的实现原理,我们可以看出KVC的性能并不如直接访问属性快,虽然这个性能消耗是微乎其微的。所以在使用KVC的时候,建议最好不要手动设置属性的setter、getter,这样会导致搜索步骤变长。
而且尽量不要用KVC进行集合操作,例如NSArray、NSSet之类的,集合操作的性能消耗更大,而且还会创建不必要的对象。
私有访问
根据上面的实现原理我们知道,KVC本质上是操作方法列表以及在内存中查找实例变量。我们可以利用这个特性访问类的私有变量,例如下面在.m中定义的私有成员变量和属性,都可以通过KVC的方式访问。
这个操作对readonly的属性,@protected的成员变量,都可以正常访问。如果不想让外界访问类的成员变量,则可以将accessInstanceVariablesDirectlygetter方法返回为NO。
Person.m文件
@interface Person(){
NSString *str;
}
@property (strong, nonatomic) NSString *testStr;
@end
testStr是私有属性,str是私有成员变量。使用KVC都可以访问到这两个。如果不想让外界访问到str成员变量,可以这样做:
@implementation Person
+ (BOOL)accessInstanceVariablesDirectly{
return NO;
}
@end
这样就访问不到私有的成员变量了。但是还是能访问到私有的属性,因为属性有getter和setter方法,KVC会先搜索访问方法,再去看accessInstanceVariablesDirectlygetter能不能访问实例变量。
KVC 的实践
KVC在实践中也有很多用处,例如UITabbar或UIPageControl这样的控件,系统已经为我们封装好了,但是对于一些样式的改变并没有提供足够的API,这种情况就需要我们用KVC进行操作了。可以自定义一个UITabbar对象,然后在内部创建自己想要的视图,并通过layoutSubviews方法在内部进行重新布局。然后通过KVC的方式,将UITabbarController的tabbar属性替换为自定义的类即可。
安全性检查
KVC存在一个问题在于,因为传入的key或keyPath是一个字符串,这样很容易写错或者属性自身修改后字符串忘记修改,这样会导致Crash。
可以利用iOS的反射机制来规避这个问题,通过@selector()获取到方法的SEL,然后通过NSStringFromSelector()将SEL反射为字符串。这样在@selector()中传入方法名的过程中,编译器会有合法性检查,如果方法不存在或未实现会报黄色警告。