iOS-机制

iOS-KVO

2020-02-26  本文已影响0人  xxxxxxxx_123

简介

先来看看官方的定义:

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.

KVC,全称是Key Value Coding,即键值编码,它是通过NSKeyValueCoding这个非正式协议启用的一种机制,遵循这个协议的对象提供了一种对属性的间接访问方式。当对象符合键值编码时,可以通过简洁,统一的消息传递接口通过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。

通俗的说,使用KVC,就可以直接通过字符串形式的key值对对象的属性进行存取操作,而不需通过调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定。

KVC是基于Objective-C的动态特性和Runtime机制。

作用

使用KVC可以实现以下的操作:

依赖KVO的技术:

KVCAPI:

访问属性的API

// setter
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
// getter
- (nullable id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

访问集合的API

以下就是一个KVC的例子:


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

@interface TPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, strong) NSArray *hobbies;
@property (nonatomic) ThreeFloats threeFloats;

@end


#import "TPerson.h"

@implementation TPerson

@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        TPerson *person = [[TPerson alloc] init];
        
    }
    return 0;
}

2020-02-22 11:37:13.611079+0800 objc-debug[59128:2222986] 

KVO的使用

1. 访问对象属性

该协议指定了方法,例如通用getter valueForKey:和通用setter setValue:forKey:,用于通过名称或键(参数化为字符串)来访问对象属性。这些方法和相关方法的默认实现使用键来定位基础数据并与基础数据进行交互。

1.1 直接访问

我们可以直接使用点语法访问对象的属性。

person.age = 12;
NSLog(@"==age==%d==", person.age);
// ==age==12==

1.2 间接访问

[person setValue:@"AAA" forKey:@"name"];
NSLog(@"==name==%@==", [person valueForKey:@"name"]);
// ==name==AAA==

2. 操作集合属性

NSArray就像其他任何属性一样,访问方法的默认实现与对象的集合属性(如对象)一起使用。此外,如果对象为属性定义了集合访问器方法,则它将启用对集合内容的键值访问。这通常比直接访问更有效,并且允许您通过标准化接口使用自定义集合对象。

person.hobbies = @[@"iOS", @"swift"];
person.hobbies[0] = @"android"; // 这行代码是错误的

2.1 整体赋值即可

NSArray *array = person.hobbies;
array = @[@"android", @"swift"];
person.hobbies = array;
NSLog(@"==hobbies==%@==", person.hobbies);
// ==hobbies==(android,swift)==

2.2 使用可变数组

NSMutableArray *mArray = [person mutableArrayValueForKey:@"hobbies"];
mArray[0] = @"android";
NSLog(@"==hobbies==%@==", person.hobbies);
// ==hobbies==(android,swift)==

同样,我们也可以直接对字典进行操作:

NSDictionary *setDict = @{@"name": @"BBB",
                          @"age": @12,
                          @"hobbies": @[@"iOS", @"swift"] };
        
// 字典转模型
[person setValuesForKeysWithDictionary:setDict];
NSLog(@"==%@==",person);

// 键数组转模型到字典
NSArray *array = @[@"name",@"age"];
NSDictionary *getDict = [person dictionaryWithValuesForKeys:array];
NSLog(@"==%@==", getDict);

3. 在集合对象上调用集合运算符

向键值编码兼容对象valueForKeyPath:发送消息时,可以在键路径中嵌入集合运算符。集合运算符是一小段关键字之一,其后跟一个at符号(@),该符号指定getter在返回数据之前应以某种方式操作数据。valueForKeyPath:提供者的默认实现NSObject此行为。

常见的运算符有@avg、@count、@max、@min、@sum

该例中我们向数组发送了length的方法,返回了每个元素的长度,发送lowercaseString方法返回了每个元素的小写。

NSArray *array = person.hobbies;
NSArray *lenStr= [array valueForKeyPath:@"length"];
NSLog(@"%@",lenStr); // (3,5)
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"%@",lowStr); // (ios,swift) 

这个例子中,我们使用集合运算符对对象age这个属性进行了操作:

NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
    TPerson *person = [[TPerson alloc] init];
    NSDictionary* dict = @{ @"name": @"CCC",
                            @"age": @(12+i) };
    [person setValuesForKeysWithDictionary:dict];
    [personArray addObject:person];
}
NSLog(@"%@", [personArray valueForKey:@"age"]);
    
float avg = [[personArray valueForKeyPath:@"@avg.age"] floatValue];
NSLog(@"%f", avg);
    
int count = [[personArray valueForKeyPath:@"@count.age"] intValue];
NSLog(@"%d", count);
    
int sum = [[personArray valueForKeyPath:@"@sum.age"] intValue];
NSLog(@"%d", sum);
    
int max = [[personArray valueForKeyPath:@"@max.age"] intValue];
NSLog(@"%d", max);
    
int min = [[personArray valueForKeyPath:@"@min.age"] intValue];
NSLog(@"%d", min);

控制台输出如下:

2020-02-22 17:48:28.799535+0800 objc-debug[60596:2386404] (12, 13, 14, 15, 16, 17)
2020-02-22 17:48:28.802529+0800 objc-debug[60596:2386404] 14.500000
2020-02-22 17:48:28.803014+0800 objc-debug[60596:2386404] 6
2020-02-22 17:48:28.803556+0800 objc-debug[60596:2386404] 87
2020-02-22 17:48:28.803871+0800 objc-debug[60596:2386404] 17
2020-02-22 17:48:28.804161+0800 objc-debug[60596:2386404] 12

4. 访问非对象属性

协议的默认实现检测非对象属性(包括简单类型和结构体),并自动将它们转化和解包为对象,以在协议接口上使用。另外,该协议声明了一种方法,该方法兼容对象针对nil通过键值编码接口在非对象属性上设置值的情况提供适当的操作。

4.1 简单类型转为NSNumber

image

4.2 结构体转为NSValue

image
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==%@==",reslut);
// ==reslut=={length = 12, bytes = 0x0000803f0000004000004040}==

ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);

// 1.000000 - 2.000000 - 3.000000

5. 通过键路径访问属性

当您具有与键值编码兼容的对象的层次结构时,也就是模型套模型,可以使用基于键路径的方法来获取或设置值。

创建一个类TStudent

@interface TStudent : NSObject

@property (nonatomic, assign) int gender;

@end

然后给TPerson添加一个属性:

@property (nonatomic, strong) TStudent *student;
TStudent *student = [[TStudent alloc] init];
person.student = student;
[person setValue:@0 forKeyPath:@"student.gender"];

KVC原理

设置值的过程

setValue:forKey:的默认实现是,使用给定键和值参数作为输入,将名为key的属性设置为value,设置的时候按照以下的顺序:

  1. 首先查找第一个名为set<Key>:或者_set<Key>的访问器。如果找到,则调用方法设置并完成。

  2. 如果没有找到简单的访问,且类方法accessInstanceVariablesDirectly返回的是YES,那就按照这个顺序去寻找一个实例变量名称类似_<key>_is<Key><key>is<Key>。如果找到,直接设置值并完成操作。

  3. 在找不到访问器或实例变量后,调用setValue:forUndefinedKey:。默认情况下会引发异常,但是NSObject的子类可能提供特定的行为。

获取值的过程

valueForKey:的默认实现是给定一个key参数作为输入,它从接收该valueForKey:调用的类实例内部执行以下过程:

  1. 按照get<Key>, <key>, is<Key>,_<key>的顺序查找对象中是否有对应的方法。

    • 如果找到了,将方法返回值带上执行步骤5
    • 如果没有找到,执行步骤2
  2. 在实例中搜索名称类似于countOf<Key>objectIn<Key>AtIndex:这种模式的方法(对应于 NSArray 类定义的原始方法)以及<key>AtIndexes:方法(对应于NSArray方法 objectsAtIndexes:)。

    • 如果找到countOf<Key>,而且还找到objectIn<Key>AtIndex:<key>AtIndexes:objectsAtIndexes:中的至少一个,则创建一个响应所有NSArray方法的集合代理对象并将其返回
    • 如果没有找到,执行步骤3
  3. 查找名为countOf<Key>enumeratorOf<Key>memberOf<Key> 这三个方法(对应于NSSet类定义的原始方法)

    • 如果找到这三个方法,则创建一个响应所有NSSet方法的代理集合对象,并返回该对象
    • 如果没有找到,执行步骤4
  4. 判断类方法accessInstanceVariablesDirectly结果,

    • 如果是YES,则按照_<key>, _is<Key>, <key>, is<Key>的顺序查找实例变量,
      • 如果找到,带上变量执行步骤5
      • 如果找不到,执行步骤6
    • 如果是NO,执行步骤6
  5. 如果属性值是对象指针,直接返回;如果属性值可以转化为NSNumber 类型,则将其转化为NSNumber类型返回;如果属性值也不能转化为NSNumber类型,则将其转化为NSValue类型返回。

  6. 调用valueForUndefinedKey:,默认情况下,这会引发一个异常,但是NSObject的子类可能提供特定的行为。

下面我们来验证一下这个过程,由于属性会自动生成setter/getter,所以我们使用成员变量进行验证。给TPerson添加以下实例变量、属性、方法:

@interface TPerson : NSObject{
    @public
    // 按照`_<key>`,`_is<Key>`,`<key>`,`is<Key>`
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

/*********设值*******/
// 按照`set<Key>:`、`_set<Key>`的顺序
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

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

/*********取值*******/
// 按照`get<Key>`, `<key>`, `is<Key>`,`_<key>`
- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

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

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

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

/*********设值*******/
[person setValue:@"DDD" forKey:@"name"];
NSLog(@"-_name-%@-_isName-%@-name-%@-isName-%@",person->_name,person->_isName,person->name,person->isName);
NSLog(@"-_isName-%@-name-%@-isName-%@",person->_isName,person->name,person->isName);
NSLog(@"-name-%@-isName-%@",person->name,person->isName);
NSLog(@"isName-%@",person->isName);

/*********取值*******/
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);

当我们执行[person setValue:@"DDD" forKey:@"name"],控制台会输出:

-[TPerson setName:] - DDD

这正好验证了第一步的第一个方法,我们再注释setName方法,运行程序,控制台输出:

-[TPerson _setName:] - DDD

注释掉setName_setName方法,运行程序,控制台输出:

-_name-DDD-_isName-(null)-name-(null)-isName-(null)

我们在注释掉NSString *_name;和第一个打印方法,运行程序,控制台输出:

-_isName-DDD-name-(null)-isName-(null)

后面几步同理。可以看出,设值顺序和我们之前的结论完全一致。接着我们来验证取值顺序,注释掉设值的相关代码,运行程序,控制台输出:

取值:getName

注释掉getName方法,运行程序,控制台输出:

取值:name

剩下的验证就不在此处做赘述。可以得出,取值顺序也和结论一致。

自定义KVC

既然我们了解了KVC的取值、设值原理和过程,那么我们可以简单的模拟一下其setValue:forKey:valueForKey:的过程,暂时忽略NSArray、NSSet的处理以及NSNumberNSValue的相互转换过程。

思路

自定义设值方法: (setValue:forKey:)

自定义取值方法: (valueForKey)

实现

- (void)customSetValue:(nullable id)value forKey:(NSString *)key{
    
    // 1:非空判断一下
    if (key == nil  || key.length == 0) return;
    
    // 2:找到相关方法 set<Key> _set<Key> setIs<Key>
    // 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 customPerformSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self customPerformSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }
    
    // 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.找相关实例变量进行赋值
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    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)customValueForKey:(NSString *)key{
    
    // 1:key 判断非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // 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<num-1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }
            for (int j = 0; j<num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }
#pragma clang diagnostic pop
    
    // 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.找相关实例变量进行赋值
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // _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)customPerformSelectorWithMethodName:(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<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}

总结

KVC是基于NSKeyValueCoding协议,核心是setValue:forKey:valueForKey:。其原理如下:

image image

Tips setNilValueForKey

当我们对非对象的属性传入nil的时候,程序会崩溃,告诉我们不能对其设置nil,这是因为在底层调用了以下方法:

- (void)setNilValueForKey:(NSString *)key

如果我们实现了这个方法,则就不会崩溃。

而当给对象的属性传入nil的时候,底层会判断直接会直接不响应也不会崩溃。setNilValueForKey只对可以转为NSNumber或者NSValue的非对象属性响应、

参考文献: Apple官方文档

上一篇 下一篇

猜你喜欢

热点阅读