iOS中KVC使用及原理解析

什么是KVC?

KVC即Key-Value Coding,意思键-值编码。iOS中键值编码机制是一种由NSKeyValueCoding非正式协议启用的机制,通过这个协议可以间接访问OC对象的属性。当一个对象符合键值编码时,它的属性可以利用字符串参数来寻址,通过一个简洁、统一的消息传递接口。这种间接访问机制补充了实例变量及其关联的访问方法所提供的直接访问。更多内容参考官方文档。

KVC的使用

通过官方文档我们知道KVC可以总结为5种使用类型,下面针对这几种使用类型制作demo:

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

@interface LNUser : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) NSInteger age;

@property (nonatomic, copy) NSArray  *friends;

+ (LNUser *)userWithName:(NSString *)name age:(NSInteger)age;

@end

@implementation LNUser

+ (LNUser *)userWithName:(NSString *)name age:(NSInteger)age
{
    LNUser *user = [[LNUser alloc] init];
    user.name = name;
    user.age = age;
    return user;
}

- (nonnull id)copyWithZone:(nullable NSZone *)zone {
    
    return self;
}

@end

@interface ViewController ()

@property (nonatomic, copy) LNUser *user;

@property (nonatomic) ThreeFloats threeFloats;

@end

使用实例代码:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    [self addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:&kCustomContext];

    // 1、Access object properties访问对象属性
    LNUser *user = [[LNUser alloc] init];
    [user setValue:@"TT" forKey:@"name"];
    NSString *name = [user valueForKey:@"name"];
    NSLog(@"name:%@", name);
    
    // 2、Manipulate collection properties 操作集合类型属性
    NSArray *friends = [NSArray arrayWithObjects:[LNUser userWithName:@"YY" age:17], [LNUser userWithName:@"WW" age:18], [LNUser userWithName:@"ZZ" age:19], nil];
    [user setValue:friends forKeyPath:@"friends"];
    NSMutableArray *mutaFriends = [user mutableArrayValueForKey:@"friends"];
    [mutaFriends addObject:[LNUser userWithName:@"DD" age:20]];
    NSLog(@"mutaFriends:%@", mutaFriends);
    
    // 3、Access properties by key path 通过keyPath访问对象
    [self setValue:user forKey:@"user"];
    [self setValue:@"我不叫TT了" forKeyPath:@"user.name"];
    NSString *currentUserName = [self valueForKeyPath:@"user.name"];
    NSLog(@"currentUserName:%@", currentUserName);
    
   
    // 4、Invoke collection operators on collection objects 唤起集合中对象操作
    // 4、1 @avg
    NSNumber *ageAverage = [mutaFriends valueForKeyPath:@"@avg.age"];
    NSLog(@"ageAverage:%@",ageAverage);
    // 4、2 @max
    NSNumber *maxAge = [mutaFriends valueForKeyPath:@"@max.age"];
    NSLog(@"maxAge:%@",maxAge);
    // 4、3 @min
    NSNumber *minAge = [mutaFriends valueForKeyPath:@"@min.age"];
    NSLog(@"minAge:%@",minAge);
    // 4、4 @sum
    NSNumber *sumAge = [mutaFriends valueForKeyPath:@"@sum.age"];
    NSLog(@"sumAge:%@",sumAge);
    // 4、5 @count
    NSNumber *userCount = [mutaFriends valueForKeyPath:@"@count"];
    NSLog(@"userCount:%@",userCount);
    
    
    // 5、Access non-object properties 访问非对象类型属性
    ThreeFloats floats = {1., 2., 3.};
    NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [self setValue:value forKey:@"threeFloats"];
    NSValue* result = [self valueForKey:@"threeFloats"];
    ThreeFloats tfResult;
    [result getValue:&tfResult];
    NSLog(@"tfResult .x:%f, .y:%f, .z:%f", tfResult.x, tfResult.y, tfResult.z);
    
}
@end
  • Access object properties访问对象属性
// 1、Access object properties访问对象属性
    LNUser *user = [[LNUser alloc] init];
    [user setValue:@"TT" forKey:@"name"];
    NSString *name = [user valueForKey:@"name"];
    NSLog(@"name:%@", name);
  • Manipulate collection properties 操作集合类型属性
// 2、Manipulate collection properties 操作集合类型属性
    NSArray *friends = [NSArray arrayWithObjects:[LNUser userWithName:@"YY" age:17], [LNUser userWithName:@"WW" age:18], [LNUser userWithName:@"ZZ" age:19], nil];
    [user setValue:friends forKeyPath:@"friends"];
    NSMutableArray *mutaFriends = [user mutableArrayValueForKey:@"friends"];
    [mutaFriends addObject:[LNUser userWithName:@"DD" age:20]];
    NSLog(@"mutaFriends:%@", mutaFriends);
  • Access properties by key path 通过keyPath访问对象
    通过路由的形式访问对象的属性。
// 3、Access properties by key path 通过keyPath访问对象
    [self setValue:user forKey:@"user"];
    [self setValue:@"我不叫TT了" forKeyPath:@"user.name"];
    NSString *currentUserName = [self valueForKeyPath:@"user.name"];
    NSLog(@"currentUserName:%@", currentUserName);
  • Invoke collection operators on collection objects 唤起集合中对象操作
    可以计算数组中某个属性的平均值、最大值、最小值、总值和数组个数。
// 4、1 @avg
    NSNumber *ageAverage = [mutaFriends valueForKeyPath:@"@avg.age"];
    NSLog(@"ageAverage:%@",ageAverage);
    // 4、2 @max
    NSNumber *maxAge = [mutaFriends valueForKeyPath:@"@max.age"];
    NSLog(@"maxAge:%@",maxAge);
    // 4、3 @min
    NSNumber *minAge = [mutaFriends valueForKeyPath:@"@min.age"];
    NSLog(@"minAge:%@",minAge);
    // 4、4 @sum
    NSNumber *sumAge = [mutaFriends valueForKeyPath:@"@sum.age"];
    NSLog(@"sumAge:%@",sumAge);
    // 4、5 @count
    NSNumber *userCount = [mutaFriends valueForKeyPath:@"@count"];
    NSLog(@"userCount:%@",userCount);
  • Access non-object properties 访问非对象类型属性
    非对象类型比如结构,可以先转成NSValue对象类型。
// 5、Access non-object properties 访问非对象类型属性
    ThreeFloats floats = {1., 2., 3.};
    NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [self setValue:value forKey:@"threeFloats"];
    NSValue* result = [self valueForKey:@"threeFloats"];
    ThreeFloats tfResult;
    [result getValue:&tfResult];
    NSLog(@"tfResult .x:%f, .y:%f, .z:%f", tfResult.x, tfResult.y, tfResult.z);

上面demo打印结果:

2021-08-15 19:36:05.871771+0800 KVC&KVODemo[26100:14206596] name:TT
2021-08-15 19:36:05.872155+0800 KVC&KVODemo[26100:14206596] mutaFriends:(
"",
"",
"",
""
)
2021-08-15 19:36:05.872336+0800 KVC&KVODemo[26100:14206596] currentUserName:我不叫TT了
2021-08-15 19:36:05.872668+0800 KVC&KVODemo[26100:14206596] ageAverage:18.5
2021-08-15 19:36:05.872832+0800 KVC&KVODemo[26100:14206596] maxAge:20
2021-08-15 19:36:05.873016+0800 KVC&KVODemo[26100:14206596] minAge:17
2021-08-15 19:36:05.873203+0800 KVC&KVODemo[26100:14206596] sumAge:74
2021-08-15 19:36:05.873352+0800 KVC&KVODemo[26100:14206596] userCount:4
2021-08-15 19:36:05.874153+0800 KVC&KVODemo[26100:14206596] tfResult .x:1.000000, .y:2.000000, .z:3.000000

KVC的底层原理

知道了KVC的用法,那么KVC是通过什么方式把key-value和对象的值给映射上的呢?接下来我们以上面代码的第一种方式——访问对象属性(Access object properties)为例详解setValue:forKey:和getValueForKey:的过程。

Search Pattern for the Basic Getter (getValueForKey:)
  • 1、直接查找get方法
    按顺序查找跟Key相关的get方法get, , is, or _,有的话就直接跳到第5步,没有就往下一步。
  • 2、 在对象中查找数组NSArray相关的方法
    通过查找对象是否有这三个方法countOf:objectInAtIndex:AtIndexes: ,如果找到了第一个和另外两个中的至少一个,创建一个集合代理对象,响应所有NSArray方法并返回它。否则,执行步骤3。
  • 3、查找结合NSSet相关的方法
    如果步骤1和步骤2都找不到,则查找名为countOfenumeratorOf,和memberOf:(对应于NSSet类定义的基本方法)的三个方法。如果找到了所有三个方法,创建一个集合代理对象响应所有的NSSet方法并返回它。否则,执行步骤4。
  • 4、间接访问成员变量
    如果即没有直接获取的方法,也不是集合类型则会判断accessInstanceVariablesDirectly是否返回YES,是的话就会按顺序查找名为 _, _is, , or `is的成员变量。查找到的话跳转到第5步,查不到跳转到第6步;
  • 5、返回值处理
    如果属性是个对象类型,直接返回;如果是一个支持NSNumber的数值类型则转化成NSNumber类型返回;如果是一个不支持NSNumber的数值类型,则转成NSValue返回。
  • 6、如果查不到就调用valueForUndefinedKey: 方法,此方法在NSObject中默认是抛出一个异常,可以重写自定义处理方式。
Search Pattern for the Basic Setter(setValue:forKey:)
  • 1、先找set相关方法
    首先会根据Key寻找对象中是否有Key名字一样的set: 或者_set方法,比如上面的demo的[user setValue:@"TT" forKey:@"name"],会先寻找对象user有没有setName:方法,有的话调用赋值,没有的话在看有没有_setName:方法,没有就继续往下查找。
  • 2、间接访问实例变量
    如果第1步中找不到,则会先通过accessInstanceVariablesDirectly(默认返回YES)判断是否可以直接访问成员变量,如果返回YES就按顺序寻找如下实例变量名称_, _is, , 或者 is。比如demo中设置user的name时,对应的就是_name,_isName,name,isName,优先级由前及后,如果找到就直接赋值,没有继续往下。
  • 3、调用setValue:forUndefinedKey:方法
    如果上面两步都没有赋值成功,那么会调用setValue:forUndefinedKey:方法。NSObject的默认实现是抛出一个异常。可以重写这个方法增加我们自定义的处理方式。

getValueForKey:中会针对集合类型做了处理,但是setValue:forKey:过程的中针对可变集合类型有另外的处理。

可变数组NSMutableArray和可变集合NSMutableSet的设置方法

参考官方文档

  • Search Pattern for Mutable Arrays
  • Search Pattern for Mutable Ordered Sets
  • Search Pattern for Mutable Set

自定义KVC

//
//  NSObject+KVC.m
//  KVC&KVODemo
//
//  Created by Lenny on 2021/8/15.
//

#import "NSObject+KVC.h"
#import 

@implementation NSObject (KVC)

- (void)custom_setValue:(nullable id)value forKey:(NSString *)key{
   
    // KVC 自定义
    // 1: 判断什么 key
    if (key == nil || key.length == 0) {
        return;
    }
    
    // 2: setter set: or _set,
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self custom_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self custom_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self custom_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"CUSTOMUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4: 间接变量
    // 获取 ivar -> 遍历 containsObjct -
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _ _is  is
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        // 4.2 获取相应的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 对相应的 ivar 设置值
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:_isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:key]) {
       Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }
    
    // 5:如果找不到相关实例
    @throw [NSException exceptionWithName:@"CUSTOMUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];  
}

- (nullable id)custom_valueForKey:(NSString *)key{
    
    // 1:刷选key 判断非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相关方法 get  countOf  objectInAtIndex
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i _is  is
    // _name -> _isName -> name -> isName
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }

    return @"";
}


#pragma mark - 相关方法
- (BOOL)custom_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
 
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (id)performSelectorWithMethodName:(NSString *)methodName{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}

- (NSMutableArray *)getIvarListName{
    
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i

你可能感兴趣的:(iOS中KVC使用及原理解析)