OC底层原理(12)-KVC

923 阅读6分钟

KVC初探

1.一般setter方法

代码:

ViewController.m

 person.name      = @"LG_Cooci";
 person.age       = 18;
 person->myName   = @"cooci";
 NSLog(@"%@ - %d - %@",person.name,person.age,person->myName);

打印结果:

Janice - 18 - ty

2.Key-Value Coding (KVC) : 基本类型赋值

代码:

ViewController.m

[person setValue:@"Janice" forKey:@"name"];
[person setValue:@19 forKey:@"age"];
[person setValue:@"ty" forKey:@"myName"];
NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]);

打印结果:

Janice - 19 - ty

3.KVC:集合类型赋值

代码:

ViewController.m

//不可变数组

person.array = @[@"1",@"2",@"3"];
NSArray *array = [person valueForKey:@"array"];
// 用 array 的值创建一个新的数组
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);

//可变数组
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"200";
NSLog(@"%@",[person valueForKey:@"array"]);

打印结果:

(
    100,
    2,
    3
)

(
    200,
    2,
    3
)

4.KVC - 访问非对象属性

代码:

LGPerson.h

#import <Foundation/Foundation.h>
#import "LGStudent.h"

NS_ASSUME_NONNULL_BEGIN

typedef struct {
    float x, y, z;
} ThreeFloats;

@interface LGPerson

@property (nonatomic) ThreeFloats threeFloats;

@end

ViewController.m

    ThreeFloats floats = {1., 2., 3.};
    NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSValue *reslut = [person valueForKey:@"threeFloats"];
    NSLog(@"%@",reslut);
    
    ThreeFloats th;
    [reslut getValue:&th] ;
    NSLog(@"%f - %f - %f",th.x,th.y,th.z);

打印结果:

{length = 12, bytes = 0x0000803f0000004000004040}
1.000000 - 2.000000 - 3.000000

5.KVC - 层层访问

    LGStudent *student = [[LGStudent alloc] init];
    student.subject    = @"语文";
    person.student     = student;
    [person setValue:@"数学" forKeyPath:@"student.subject"];
    NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

打印结果:

数学

KVC原理设值过程

这个过程中主要用成员变量来探索,为什么要使用成员变量呢,因为属性在赋值的过程中本来就会生成getter 和 setter 方法了,所以再用属性探索KVC赋值,会不好分辩。

根据官方文档可知 KVC setter 过程中查找赋值的方法如下:

1、set<Key>: 或则 _set<key>

2、如果找不到第一步,查看 accessInstanceVariablesDirectly 返回是否为YES,如果返回YES,就继续查找一下方法:_<key>, _is<key>,<key>,或则 is<key>

依次来验证一下:

1、先找setter

代码:

LGPerson.h

#import <Foundation/Foundation.h>
#import "LGStudent.h"

NS_ASSUME_NONNULL_BEGIN

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

LGPerson.m

#import "LGPerson.h"

@implementation LGPerson

#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//MARK: - setKey. 的流程分析
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

@end

ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGPerson *person = [[LGPerson alloc] init];
    
    // 1: KVC - 设置值的过程
    [person setValue:@"Janice" forKey:@"name"];

    NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
    NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
    NSLog(@"%@-%@",person->name,person->isName);
    NSLog(@"%@",person->isName);
}

运行结果:

2、 当 accessInstanceVariablesDirectly 为 YES

LGPerson.m 中代码稍微做一点改动,如下:

//MARK: - setKey. 的流程分析
//注释掉
//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

打印结果:

这里走的就是 _setName 方法

然后代码再做一点改动,如下

//MARK: - setKey. 的流程分析
//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)_setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

打印结果:

然后这里就调用了 setIsName

2、 当 accessInstanceVariablesDirectly 为 NO 时,做如下改动

#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
    return NO;
}

//MARK: - setKey. 的流程分析
//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)_setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)setIsName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

打印结果:

崩溃了

以上,打印估计有注意到了我下面打印的值全部都为 null,这是因为在重写方法时,并没有复制的哈,如_name = name;

下面将重写方法全都注释掉,如下,再来打印成员变量的值,再将 accessInstanceVariablesDirectly 然后YES。

代码:

LGPerson.m

#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//MARK: - setKey. 的流程分析
//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)_setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)setIsName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

ViewController.m

#import "ViewController.h"
#import "LGPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGPerson *person = [[LGPerson alloc] init];
    
    // 1: KVC - 设置值的过程
    [person setValue:@"Janice" forKey:@"name"];

    NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
    NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
    NSLog(@"%@-%@",person->name,person->isName);
    NSLog(@"%@",person->isName);
}

打印结果:

可以看出只有第一个成员有值,然后将第一个成员变量注释掉,并将第一个打印注释掉

打印: LGPerson.h

#import <Foundation/Foundation.h>
#import "LGStudent.h"

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject{
    @public
//     NSString *_name;
     NSString *_isName;
     NSString *name;
     NSString *isName;
}
@end

ViewController.m

#import "ViewController.h"
#import "LGPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGPerson *person = [[LGPerson alloc] init];
    
    // 1: KVC - 设置值的过程
    [person setValue:@"Janice" forKey:@"name"];

//    NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
    NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
    NSLog(@"%@-%@",person->name,person->isName);
    NSLog(@"%@",person->isName);
}

打印结果:

再验证最后一个

#import <Foundation/Foundation.h>
#import "LGStudent.h"

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject{
    @public
//     NSString *_name;
//     NSString *_isName;
//     NSString *name;
     NSString *isName;
}
@end

ViewController.m

#import "ViewController.h"
#import "LGPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGPerson *person = [[LGPerson alloc] init];
    
    // 1: KVC - 设置值的过程
    [person setValue:@"Janice" forKey:@"name"];

//    NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
//    NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
//    NSLog(@"%@-%@",person->name,person->isName);
 //   NSLog(@"%@",person->isName);
}

打印结果:

以上打印验证了只有,在编译期过程中,本来只有一个name,但是还会生成 其他变量,如_name, 布尔值时,就有 isOpen,等,这就归功于 runtime的作用。

KVC原理取值过程

官方文档的查找流程如下,get<key>, <key>, is<key>,_<key>

代码:

LGPerson.h

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

LGPerson.m

- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}

ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGPerson *person = [[LGPerson alloc] init];
    // 2: KVC - 取值的过程
    
    //先赋值不同的值
     person->_name = @"_name";
     person->_isName = @"_isName";
     person->name = @"name";
     person->isName = @"isName";
    
    //取值
     NSLog(@"取值:%@",[person valueForKey:@"name"]);
}

打印结果:

打印了 getName 的方法名,说明在找变量之前先找的方法。

再验证一个,就将getName注释掉,走下一个方法

LGPerson.m

//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,

//- (NSString *)getName{
//    return NSStringFromSelector(_cmd);
//}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}

打印结果

KVC设置与取值总结:

KVC 设置过程

1、判断是否存在'set<key>' 或者 '_set<key>'(带下划线的属性)'setls<key>'

2、如果没有条件 1,(简单访问方式)

2.1、判断 'accessInstanceVariablesDirectly' 是否存在,返回 'YES'

2.2、判断 '_<key>','_is<key>','<key>','is<key>'等实例变量

2.3、直接给这些实例变量设置

3、'setValue:forUndefinedKey:'报错!

KVC 取值过程:

1、如果找到'get<key>',<key>,'is<key>','_<key>'这几个方法就跳到 '第五步'

2、如果没有条件 1 ,开始是否是 NSArray 判断

3、是否是 NSSet 判断

4、非集合类型

4.1、'accessInstanceVariablesDirectly' 返回 YES

4.2、'_<key>,_is<key>,,is<key>'这些实例属性

4.3、如果找到,直接获取实例变量的值,然后继续执行步骤 5

5、细节处理

5.1、如果检索到的属性值是对象指针,则只需返回结果

5.2、如果该值是 NSNumber 支持的标量类型,则将其存储在 NSNumber 实例中并返回它

5.3、如果结果是NSNumber 不支持的标量类型,则转换为 NSValue 对象并返回

6、'valueForUndefineKey'报错!!!

7、集合类型的还需要操作!

KVC使用小技巧

1、KVC 自动转换类型

如,

[person setValue:@"20" forKey:@"age"];

age 为 int 类型,但是给了一个 NSString 类型,于是,便会自动转换类型为 __NSCFNumber

2、可以对int,NSValue 类型设置空值,对NSString设空值变不会走设置的任何方法

当找不到时可以用一下方法可以检测到

- (void)setNilValueForKey:(NSString *)key;

3、找不到Key

当找不到时可以用一下方法可以检测到

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

4、取值时 - 找不到 key

当找不到时可以用一下方法可以检测到

- (nullable id)valueForUndefinedKey:(NSString *)key;

5、键值验证

在此方法可以做一些容错处理

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;