美文网首页面试OC运行时
iOS KVC全过程详解

iOS KVC全过程详解

作者: 萨缪 | 来源:发表于2019-10-05 20:36 被阅读0次

    什么是KVC?

    KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,很多高级的iOS开发技巧都是基于KVC实现的。目前网上关于KVC的文章在非常多,有的只是简单地说了下用法,我会运用图解的方式写下这遍文章就是为了让大家更好的理解。

    KVC方法全览

    KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量。

    image

    KVC基础操作

    KVC取值

    取值方法
    1. 通过key
    - (nullable id)valueForKey:(NSString *)key;                          //直接通过Key来取值
    
    
    1. 通过keyPath
    - (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通过KeyPath来取值
    
    
    基于getter取值底层实现

    当调用valueForKey的代码时,其搜索方式如下:

    你需要先看一下这张流程图,大致知道如何运转的,之后再看文字描述,仔细了解其机制

    image
    1. 通过getter方法搜索实例,按照get<Key>, <key>, is<Key>, _<key>的顺序查找getter`方法。如果发现符合的方法,就调用对应的方法并拿着结果跳转到第五步。否则,就继续到下一步。

    2. 如果没有找到简单的getter方法,则搜索其匹配模式的方法countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:

      如果找到其中的第一个和其他两个中的一个,则就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类)。或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex<Key>AtIndexes这几个方法组合的形式调用。否则,继续到第三步。

      代理对象随后将NSArray接收到的countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:的消息给符合KVC规则的调用方。

      当代理对象和KVC调用方通过上面方法一起工作时,就会允许其行为类似于NSArray一样。

    3. 如果没有找到NSArray简单存取方法,或者NSArray存取方法组。那么会同时查找countOf<Key>enumeratorOf<Key>memberOf<Key>:命名的方法。

      如果找到三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回。否则,继续执行第四步。

      给这个代理对象发NSSet的消息,就会以countOf<Key>enumeratorOf<Key>,memberOf<Key>组合的形式调用。

    4. 如果没有发现简单getter方法,或集合存取方法组,以及接收类方法accessInstanceVariablesDirectly是返回YES的。搜索一个名为_<key>_is<Key><key>is<Key>的实例,根据他们的顺序。

      如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步,如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:

    5. 如果取回的是一个对象指针,则直接返回这个结果。
      如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。
      如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。

    6. 如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法。

    KVC设值

    赋值方法
    1. 通过key
    • 直接将属性名当做key,并设置value,即可对属性进行赋值。

      - (void)setValue:(nullable id)value forKey:(NSString *)key;          //通过Key来设值
      
      
    1. 通过keyPath
    • 除了对当前对象的属性进行赋值外,还可以对其更“深层”的对象进行赋值。KVC进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。例如Person属性中有name属性,我就可以通过Person.name进行赋值

      - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值
      
      
    基于setter赋值底层实现

    这是setValue:forKey:的默认实现,给定输入参数valuekey。试图在接收调用对象的内部,设置属性名为keyvalue,通过下面的步骤

    你需要先看一下这张流程图,大致知道如何运转的,之后再看文字描述,仔细了解其机制

    image
    1. 查找set<Key>:_set<Key>命名的setter,按照这个顺序,如果找到的话,代码通过setter方法完成设置。
    2. 如果没有找到setter方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly的返回值,如果accessInstanceVariablesDirectly类属性返回YES,则查找一个命名规则为_<key>_is<Key><key>is<Key>的实例变量。根据这个顺序,如果发现则将value赋值给实例变量,如果返回值为NO,KVC会执行setValue:forUndefinedKey:方法。
    3. 如果没有发现setter或实例变量,则调用setValue:forUndefinedKey:方法,并默认提出一个异常,但是一个NSObject的子类可以提出合适的行为。

    KVC批量操作

    • 在对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含keyvalue的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将valueUser对象的属性赋值。

      //创建一个model模型,里面的字符串名称必须和key的名称对应,不然该方法会崩溃
      @interface PersonModel : NSObject
      @property (nonatomic, copy) NSString *key1;
      @property (nonatomic, copy) NSString *key2;
      @property (nonatomic, copy) NSString *id;
      @property (nonatomic, copy) NSString *key3;
      @property (nonatomic, copy) NSString *other;
      @end
      
      PersonModel *person = [[PersonModel alloc] init];
      //1.这是直接赋值,数据量小会很简单,但是数据量一多就很麻烦,就像我们进行网络请求时
      person.key1 = dictionary[@"key1"];
      person.key2 = dictionary[@"key2"];
      person.key3 = dictionary[@"key3"];
      
      //2.通过下面该方法可以批量赋值
      //2.1如果model里面的string不存在于dictionary中,输出结果为null;
      [person setValuesForKeysWithDictionary:dictionary];
      NSLog(@"\n%@\n%@\n%@\n%@\n", person.key1,person.key2,person.key3,person.other);
      
      //输出结果
      test1
      test2
      test3
      (null)
      
      //2.2如果dictionary中有的元素,moedl中没有运行会直接出错,那么我们应该怎么解决?
      //我们需要实现setValue:forUndefinedKey:这个方法能过滤掉不存在的键值
      -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
        //这里我们不需要写任何内容
      }
      person.key1 = dictionary[@"key1"];
      person.key2 = dictionary[@"key2"];
      person.key3 = dictionary[@"key3"];
      [person setValuesForKeysWithDictionary:dictionary];
      NSLog(@"\n%@\n%@\n%@\n", person.key1,person.key2,person.key3);
      
      //输出结果
      test1
      test2
      test3
      
      //2.3如果dictionar中的key与model中的变量名字不同,怎么赋值?
      //还是从setValue:forUndefinedKey:这个方法入手
      -(void)setValue:(id)value forUndefinedKey:(NSString *)key{
        if ([key isEqualToString:@"key2"]) {
          self.id = value;
      }
      person.key1 = dictionary[@"key1"];
      person.id = dictionary[@"key2"];
      person.key3 = dictionary[@"key3"];
      [person setValuesForKeysWithDictionary:dictionary];
      NSLog(@"\n%@\n%@\n%@\n", person.key1,person.id,person.key3);
      
      //输出结果
      test1
      test2
      test3
      
      

    KVC集合属性操作

    KVC提供的valueForKeyPath:方法非常强大,可以通过该方法对集合对象进行“深入”操作,在其keyPath中嵌套集合运算符,例如求一个数组中对象某个属性的count。(集合对象主要指NSArrayNSSet,但不包括NSDictionary)

    image

    上面表达式主要分为三部分,left部分是要操作的集合对象,如果调用KVC的对象本来就是集合对象,则left可以为空。中间部分是表达式,表达式一般以@符号开头。后面是进行运算的属性。

    • 为了验证操作符,我们需要先建立一个Model类
    @interface Transaction : NSObject
    @property (nonatomic, strong) NSString *payee;
    @property (nonatomic, strong) NSNumber *amount;
    @property (nonatomic, strong) NSDate *date;
    @end
    
    @interface BankAccount : NSObject
    @property (nonatomic, strong) NSArray *transcationArray;
    @end
    
    

    集合操作符

    处理集合包含的对象,并根据操作符的不同返回不同的类型,返回值以NSNumber为主。

    //@avg用来计算集合中right keyPath指定的属性的平均值
    NSNumber *transactionAverage = [bankAccount.transcationArray valueForKeyPath:@"@avg.amount"];
    NSLog(@"@avg = %@", transactionAverage);
    
    //@count用来计算集合的总数
    NSNumber *numberOfTransactions = [bankAccount.transcationArray valueForKeyPath:@"@count"];
    NSLog(@"@count = %@", numberOfTransactions);
    //备注:@count操作符比较特殊,它不需要写right keyPath,即使写了也会被忽略。
    
    //@sum用来计算集合中right keyPath指定的属性的总和。
    NSNumber *amountSum = [bankAccount.transcationArray valueForKeyPath:@"@sum.amount"];
    NSLog(@"@sum = %@", amountSum);
    
    //@max用来查找集合中right keyPath指定的属性的最大值
    NSNumber *amountMax = [bankAccount.transcationArray valueForKeyPath:@"@max.amount"];
    NSLog(@"@max = %@", amountMax);
    
    //@min用来查找集合中right keyPath指定的属性的最小值。
    NSNumber *amountMin = [bankAccount.transcationArray valueForKeyPath:@"@min.amount"];
    NSLog(@"@min = %@", amountMin);
    
    

    数组操作符

    根据操作符的条件,将符合条件的对象包含在数组中返回。

    //@unionOfObjects将集合对象中,所有payee对象放在一个数组中并返回
    NSArray *payees = [bankAccount.transcationArray valueForKeyPath:@"@unionOfObjects.payee"];
    NSLog(@"@unionOfObjects = %@", payees);
    
    //@distinctUnionOfObjects将集合对象中,所有payee对象放在一个数组中,并将数组进行去重后返回。
    NSArray *distinctPayees = [bankAccount.transcationArray valueForKeyPath:@"@distinctUnionOfObjects.payee"];
    NSLog(@"@distinctUnionOfObjects = %@", distinctPayees);
    //注意:以上两个方法中,如果操作的属性为nil,在添加到数组中时会导致Crash。
    
    

    嵌套操作符

    处理集合对象中嵌套其他集合对象的情况,返回结果也是一个集合对象。

    //@distinctUnionOfArrays是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个数组中,并进行排重。
    NSArray *collectedPayees = [allArray valueForKeyPath:@"@unionOfArrays.payee"];
    NSLog(@"@unionOfArrays = %@", collectedPayees);
    
    //@distinctUnionOfSets是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个set中,并进行排重。
    NSArray *collectedDistinctPayees = [allArray valueForKeyPath:@"@distinctUnionOfArrays.payee"];
    NSLog(@"@distinctUnionOfArrays = %@", collectedDistinctPayees);
    
    

    KVC与容器类

    对象的属性可以是一对一的,也可以是一对多的。一对多的属性要么是有序的(数组),要么是无序的(集合)。

    ⚠️:根据KVO的实现原理,是在运行时生成新的子类并重写其setter方法,在其内容发生改变时发送消息。但这只是对属性直接进行赋值会触发,如果属性是容器对象,对容器对象进行addremove操作,则不会调用KVO的方法。可以通过KVC对应的API来配合使用,使容器对象内部发生改变时也能触发KVO

    在进行容器对象操作时,先通过key或者keyPath获取集合对象,然后再对容器对象进行addremove等操作时,就会触发KVO的消息通知了。

    KVC与有序容器(NSMutableArray)

    取值方法
    1. 通过key
    - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
    //该方法返回一个可变有序数组
    
    
    1. 通过keyPath
    - (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
    //该方法返回一个可变有序数组
    
    
    NSMutableArray取值底层实现

    当调用mutableArrayValueForKey的代码时,其搜索方式如下:

    你需要先看一下这张流程图,大致知道如何运转的,之后再看文字描述,仔细了解其机制

    image
    1. 搜索insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes 格式的方法
      如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableArray所有方法代理集合(类名是NSKeyValueFastMutableArray),那么给这个代理集合发送NSMutableArray的方法,以insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes组合的形式调用。

      当对象接收一个mutableArrayValueForKey:消息并实现可选替换方法,例如replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:方法,代理对象会在适当的情况下使用它们,以获得最佳性能。

    2. 如果上步的方法没有找到,则搜索set<Key>: 格式的方法,如果找到,那么发送给代理集合的NSMutableArray最终都会调用set<Key>:方法。

      也就是说,mutableArrayValueForKey:取出的代理集合修改后,用set<Key>: 重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。

    3. 如果上一步的方法还还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_<key>,<key>,的顺序搜索成员变量名,如果找到,那么发送的NSMutableArray消息方法直接交给这个成员变量处理。

    4. 如果还是找不到,则调用valueForUndefinedKey:

    KVC与无序容器(NSMutableSet)

    取值方法
    1. 通过key
    - (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
    //方法返回一个可变的无序数组
    
    
    1. 通过keyPath
    - (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
    //方法返回一个可变的无序数组
    
    
    NSMutableSet取值底层实现

    当调用NSMutableSet的代码时,其搜索方式如下:

    你需要先看一下这张流程图,大致知道如何运转的,之后再看文字描述,仔细了解其机制

    image
    1. 搜索addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key> 格式的方法
      如果至少找到一个insert方法和一个remove方法,那么同样返回一个可以响应NSMutableSet所有方法代理集合(类名是NSKeyValueFastMutableSet2),那么给这个代理集合发送NSMutableSet的方法,以addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key>组合的形式调用。还有两个可选实现的接口:intersect<Key> , set<Key>:
    2. 如果receiverManagedObject,那么就不会继续搜索。
    3. 如果上一步的方法没有找到,则搜索set<Key>: 格式的方法,如果找到,那么发送给代理集合的NSMutableSet最终都会调用set<Key>:方法。 也就是说,mutableSetValueForKey取出的代理集合修改后,用set<Key>: 重新赋值回去去。这样做效率会低很多。所以推荐实现上面的方法。
    4. 如果上一步的方法还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),会按_<key>,<key>的顺序搜索成员变量名,如果找到,那么发送的NSMutableSet消息方法直接交给这个成员变量处理。
    5. 如果还是找不到,调用valueForUndefinedKey:
      可见,除了检查receiverManagedObject以外,其搜索顺序和mutableArrayValueForKey基本一至

    KVC异常处理

    1. key或者keyPath发生错误

    当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个NSUndefinedKeyException的异常,并且应用程序Crash

    我们可以重写下面两个方法:

    - (nullable id)valueForUndefinedKey:(NSString *)key;
    - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
    
    
    1. 传参为nil

    通常情况下,KVC不允许你要在调用setValue:属性值 forKey:(或者keyPath)时对非对象传递一个nil的值。因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。

    我们可以重写这个方法:

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

    KVC处理非对象

    KVC是支持基础数据类型和结构体的,可以在settergetter的时候,通过NSValueNSNumber来转换为OC对象。该方法valueForKey:总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber或者NSValue对象。这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开发者需要手动转换成原来的类型。尽管valueForKey:会自动将值类型封装成对象,但是setValue:forKey:却不行。你必须手动将值类型转换成NSNumber或者NSValue类型,才能传递过去。

    • 可以调用initWithBool:方法对基础数据类型进行包装
    @property (nonatomic, assign, readonly) BOOL boolValue;
    - (NSNumber *)initWithBool:(BOOL)value 
    
    

    KVC属性验证

    KVC提供了属性值,用来验证key对应的Value是否可用的方法

    • 在调用KVC时可以先进行验证,验证通过下面两个方法进行,支持keykeyPath两种方式。验证方法默认实现返回YES,可以通过重写对应的方法修改验证逻辑。

      验证方法需要我们手动调用,并不会在进行KVC的过程中自动调用。

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

    这个方法的默认实现是去探索类里面是否有一个这样的方法:-(BOOL)validate<Key>:error:如果有这个方法,就调用这个方法来返回,没有的话就直接返回YES

    @implementation Address
    -(BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{  //在implementation里面加这个方法,它会验证是否设了非法的value
        NSString* country = *value;
        country = country.capitalizedString;
        if ([country isEqualToString:@"Japan"]) {
            return NO;                                                                             //如果国家是日本,就返回NO,这里省略了错误提示,
        }
        return YES;
    }
    @end
    NSError* error;
    id value = @"japan";
    NSString* key = @"country";
    BOOL result = [add validateValue:&value forKey:key error:&error]; //如果没有重写-(BOOL)-validate<Key>:error:,默认返回Yes
    if (result) {
        NSLog(@"键值匹配");
        [add setValue:value forKey:key];
    }
    else{
        NSLog(@"键值不匹配"); //不能设为日本,其他国家都行
    }
    NSString* country = [add valueForKey:@"country"];
    NSLog(@"country:%@",country);
    //打印结果 
    KVCDemo[867:58871] 键值不匹配
    KVCDemo[867:58871] country:China
    
    

    KVC适用场景

    动态的取值和设值

    利用KVC动态的取值和设值是最基本的用途了。相信每一个iOS开发者都能熟练掌握

    Model和字典转换

    在上面KVC批量操作已阐述

    用KVC来访问和修改私有变量

    根据上面的实现原理我们知道,KVC本质上是操作方法列表以及在内存中查找实例变量。我们可以利用这个特性访问类的私有变量,例如下面在.m中定义的私有成员变量和属性,都可以通过KVC的方式访问。

    这个操作对readonly的属性,@protected的成员变量,都可以正常访问。如果不想让外界访问类的成员变量,则可以将accessInstanceVariablesDirectly属性赋值为NO

    修改一些控件的内部属性

    这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些控件的API,这样我们就无法正常地访问和修改这些控件的样式。而KVC在大多数情况可下可以解决这个问题。

    相关文章

      网友评论

        本文标题:iOS KVC全过程详解

        本文链接:https://www.haomeiwen.com/subject/rssypctx.html