iOS - KVC底层应用

之前简单得讲过一些KVC的用法,但是并不能深入理解KVC内部实现及其原理,下面主要讲下KVC的底层原理。

取值 valueForKey:

在使用KVC取值的时候,使用valueForKey:方法,该方法会返回一个id类型的对象,那么它的内部会怎么处理的呢?现在我们使用该方法:

    Teacher *teacher = [[Teacher alloc] init];
    NSString *name = [teacher valueForKey:@"name"];    
    NSLog(@"%@",name);

分析:当使用KVC获取成员变量的值时,其内部会首先去找getter方法- (NSString *)getName,如果有该方法直接调用;如果没有就去找getter方法- (NSString *)name,如果有直接调用;如果没有就去找getter方法- (NSString *)getIsName,如果有直接调用;如果没有就去找getter方法:- (NSString *)isName,如果有直接调用;如果没有这里就会出现转折,程序不会再去找getter方法,而是去找下面这两个方法:

// 数组元素数
- (NSInteger)countOfName {
    return 5;
}
// 数组内容
- (id)objectInNameAtIndex:(NSUInteger)index {
    if (index == 0) {
        return @"Michael";
    }
    return @"Tom";
}

如果找到了这个两个方法,就会返回一个数组类型:
在这里插入图片描述
这里的场景虽然还没有遇到过,但是KVC内部确实是在没有访问到getter方法的时候,会访问这个两个方法,如果有直接返回;如果没有就开始访问成员变,在访问成员变量之前会有个判断方法:+ (BOOL)accessInstanceVariablesDirectly,该方法默认是YES,如果重写该方法并返回NO,那么将失去访问成员变量的权限,也就是说在没找到getter方法,也没找到数组方法的时候程序就会异常:valueForUndefinedKey:。下面讨论在accessInstanceVariablesDirectly:返回YES的情况,开始去找成员变量,具体用法如下:

// Teacher.h
@interface Teacher : NSObject
{
    NSString * _name;
    NSString * _isName;
    NSString * name;
    NSString * isName;
}
@end

// Teacher.m
@implementation Teacher

- (instancetype)init {
    if (self = [super init]) {
        _name = @"_name";
        _isName = @"_isName";
        name = @"name";
        isName = @"isName";
    }
    return self;
}

@end

上例中在Teacher类中定义了四个成员变量,然后使用KVC取出成员变量name的值,会打输出什么呢?输出结果是:@"_name";如果将成员变量_name及其初始化值注释,输出又会如何呢?输出结果是:@"_isName"…以此类推。结论很出人意料,虽然使用KVC获取的是成员变量name的值,但是却可以获取到四种值,它首先会到对象的成员变量中寻找名为“_key”的成员变量,如果有就取出值;如果没有就去找名为“_isKey”的成员变量,如果有就取出值;如果没有就去找名为“key”的成员变量,如果有就取出值;如果没有就去找名为“isKey”的成员变量,如果有就取出值;如果没有程序会crash,报错原因是:
valueForUndefinedKey:

reason: '[<Teacher 0x60000118fd50> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'

那么如果我们不想让程序crash,可以重写valueForUndefinedKey:方法:

- (id)valueForUndefinedKey:(NSString *)key {
    return nil;
}

此时即使key不存在,会返回null,而不会异常。

设值 setValue: forKey:

前面说使用valueForKey:方法获取值,现在说使用setValue: forKey:设值,那么该方法底层会怎么执行呢?首先回去找setter方法:

- (void)setName:(NSString *)name {
    NSLog(@"set name");
}

如果找到没找到setName:,就会去找:

- (void)setIsName:(NSString *)name {
    NSLog(@"set isName");
}

如果没找setIsName:方法,这里不会再去找别的setter方法,而是去查找成员变量,在查找之前还是需要看看accessInstanceVariablesDirectly:的返回值,如果返回NO,则失去访问成员变量的权限,程序creash;如果返回YES,就去查找成员变量,访问成员变量的优先级和valueForKey:方法一样(_key > _isKey > key > isKey),如果在查找成员变量时也没有找到key,此时程序crash。
为了避免crash,我们重写setValue: forUndefinedKey:方法:

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"没有找到这个%@",key);
}

注意:如果成员变量是基础数据类型(int、float等),在使用setValue: forKey:设值时,不能设值为nil,不然程序crash,异常原因setNilValueForKey

reason: '[<Teacher 0x6000021b72a0> setNilValueForKey]: could not set nil as the value for the key age.'

这时我们可以重写setNilValueForKey方法,可以避免这种异常:

- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能将 %@ 设置为nil",key);
}

我们会注意到valueForKey:方法返回值是id类型的,如果我们成员变量是基础数据类型而不是对象类型,这该如何处理呢?对于KVC,Cocoa会自动装箱和开箱标量值,在取值的时候其实返回的是NSNumber类型。

监听数组 mutableArrayValueForKey:

当数组受到KVO监听时,如果向数组里面添加元素会被监听到吗?例如:

[_teacher addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"new value %@",change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 监听数组的变化
    [_teacher.array addObject:@"1"];
}
- (void)dealloc {
    [_teacher removeObserver:self forKeyPath:@"array"];
}

当向数组里面添加元素时,显然没有被监听到,这时候需要用到KVC的mutableArrayValueForKey:方法,该方法返回一个可变数组,然后再向里面添加元素:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 监听数组的变化
    //[_teacher.array addObject:@"1"];
     static int i = 0;
    [[_teacher mutableArrayValueForKey:@"array"] addObject:@(i++)];
    NSLog(@"%@",[_teacher mutableArrayValueForKey:@"array"]);
}

KVC的快速运算

先看一个例子:

    NSMutableArray *array = [NSMutableArray array];
    for (int i = 0; i < 10; i++) {
        Teacher *t = [[Teacher alloc] init];
        t.age = i;
        [array addObject:t];
    }

利用KVC打印数组的个数:

NSLog(@"%@",[array valueForKey:@"@count"]);

使用@count运算符来求出数组个数,还有一些常见操作符

  • @sum:求和运算符;
  • @min:求最小值运算符;
  • @max:求最大值运算符;
  • @avg:求平均数运算符。
    NSLog(@"%@",[array valueForKeyPath:@"@sum.age"]);//求和
    NSLog(@"%@",[array valueForKeyPath:@"@min.age"]);//最小值
    NSLog(@"%@",[array valueForKeyPath:@"@max.age"]);//最大值
    NSLog(@"%@",[array valueForKeyPath:@"@avg.age"]);//平均值
    //获取到所有age的一个集合(非数组);特点:无序、不重复
    NSLog(@"%@",[array valueForKeyPath:@"@distinctUnionOfObjects.age"]);

注意:这里使用的是valueForKeyPath:方法,表示键路径,可以在对象和不同变量之间用圆点隔开,这些键路径的深度是任意的,具体取决于对象的复杂度,常用于对象的符合和快速运算,例如一个Person类中复合一个Teacher类,在Teacher类中再复合一个Student类,这时利用KVC取Student类中的一个成员变量的值,可以表示为:

NSString *name = [person valueForKeyPath:@"teacher.student.name"];

同样的还有setValue: forKeyPath:修改成员变量值的方法:

[person setValue:@"John" forKeyPath:@"teacher.student.name"];

简单实现 setValue: forKey:

下面自己简单的实现以下setValue: forKey:valueForKey:方法,创建一个NSObject的分类:

-(void)YZ_setValue:(id)value forKey:(NSString *)key
{
    if (key == nil || key.length == 0) {
        return;
    }
    
    // 寻找setKey方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(setKey)]) {
        [self performSelector:NSSelectorFromString(setKey) withObject:value];
        return;
    }
    
    // 寻找setIsKey方法
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(setIsKey)]) {
        [self performSelector:NSSelectorFromString(setIsKey) withObject:value];
        return;
    }
    
    // setter方法都没有找到去找成员变量
    // 先判断访问成员变量权限
    if ([[self class] accessInstanceVariablesDirectly]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        
        // _key
        for (NSInteger i = 0;  i< outCount; i++) {
            Ivar ivar = ivars[i];
            // 成员变量名称
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
                // 给成员变量赋值
                object_setIvar(self, ivar, value);
                return;
            }
        }
        // _isKey
        for (NSInteger i = 0;  i< outCount; i++) {
            Ivar ivar = ivars[i];
            // 成员变量名称
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"_Is%@",key.capitalizedString]]) {
                // 给成员变量赋值
                object_setIvar(self, ivar, value);
                return;
            }
        }
        // key
        for (NSInteger i = 0;  i< outCount; i++) {
            Ivar ivar = ivars[i];
            // 成员变量名称
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:key]) {
                // 给成员变量赋值
                object_setIvar(self, ivar, value);
                return;
            }
        }
        // isKey
        for (NSInteger i = 0;  i< outCount; i++) {
            Ivar ivar = ivars[i];
            // 成员变量名称
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"is%@",key.capitalizedString]]) {
                // 给成员变量赋值
                object_setIvar(self, ivar, value);
                return;
            } 
        }
        
        // 释放(new copy create)
        free(ivars);
        
    } else {
        // 不允许访问成员变量,抛出异常
        NSException *exception = [NSException exceptionWithName:@"YZKVC Exception" reason:@"accessInstanceVariablesDirectly method retun NO!" userInfo:nil];
        @throw exception;
        return;
    }
    
}


- (id)YZ_valueForKey:(NSString *)key {
    
    if (key == nil || key.length == 0) {
        return nil;
    }
    
    // 先找getter方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }
    
    if ([self respondsToSelector:NSSelectorFromString(key)]) {
        return [self performSelector:NSSelectorFromString(key)];
    }
    
    NSString *getIsKey = [NSString stringWithFormat:@"getIs%@",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(getIsKey)]) {
        return [self performSelector:NSSelectorFromString(getIsKey)];
    }
    
    // 没有查找到getter方法
    NSString *countName = [NSString stringWithFormat:@"countOf%@",key.capitalizedString];
    NSString *objectInName = [NSString stringWithFormat:@"objectIn%@AtIndex:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(countName)] && [self respondsToSelector:NSSelectorFromString(objectInName)]) {
        NSMutableArray *array = [NSMutableArray array];
        NSInteger count = [self performSelector:NSSelectorFromString(countName)];
        for (NSInteger i = 0; i < count; i++) {
             // 这里好像有点问题
            id objc = [self performSelector:NSSelectorFromString(objectInName) withObject:@(i)];
            NSLog(@"%@ %d",objc,i);
            [array addObject:objc];
        }
        return [NSArray arrayWithArray:array];
    }
    
    // 去找成员变量
    if ([[self class] accessInstanceVariablesDirectly]) {
        // 优先级与YZ_setValue: forKey: 一致
        unsigned int outCount = 0;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        
        // _key
        for (NSInteger i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
                return object_getIvar(self, ivar);
            }
        }
        
        // _isKey
        for (NSInteger i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"_is%@",key.capitalizedString]]) {
                return object_getIvar(self, ivar);
            }
        }
        
        // key
        for (NSInteger i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:key]) {
                return object_getIvar(self, ivar);
            }
        }
        
        // isKey
        for (NSInteger i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            const char * name = ivar_getName(ivar);
            NSString *keyName = [NSString stringWithUTF8String:name];
            if ([keyName isEqualToString:[NSString stringWithFormat:@"is%@",key.capitalizedString]]) {
                return object_getIvar(self, ivar);
            }
        }
        
        free(ivars);
    } else {
        NSException *exception = [NSException exceptionWithName:@"YZKVC Exception" reason:@"accessInstanceVariablesDirectly method retun NO!" userInfo:nil];
        @throw exception;
        return nil;
    }
    
    return nil;
}

调用:

    Teacher *teacher = [[Teacher alloc] init];
    [teacher YZ_setValue:@"John" forKey:@"name"];    
    NSLog(@"%@",[teacher YZ_valueForKey:@"name"]);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值