KVC

一、概念:

KVC(Key-value coding):鍵值對編碼,也就是咱們能夠經過變量的名稱來讀取或者修改它的值,而不須要調用明確的存取方法。這樣就能夠在運行時動態地訪問和修改對象的屬性。而不是在編譯時肯定。對於類裏的私有屬性,Objective-C是沒法直接訪問的,可是KVC是能夠的數組

做用:框架

  • 取值和賦值(開發中基本不用)
  • 獲取對象私有變量的值.(常用,例如UIPageContorl分頁, 設置圓點爲圖片)
  • 改變對象私有變量的值(常用)
  • 簡單的字典轉模型(偶爾使用)
  • 模型轉字典
  • 批量取值

kvc的經常使用方法有:ide

//經過Key來設值
- (void)setValue:(nullable id)value forKey:(NSString *)key; 
//經過KeyPath來設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

//直接經過Key來取值
- (nullable id)valueForKey:(NSString *)key;
//經過KeyPath來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;       

//默認返回YES,表示是否容許直接訪問變量  也就是若是沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設置成NO就不這樣搜索
+ (BOOL)accessInstanceVariablesDirectly;

關於key和keyPath的區別:函數

1.在修改一個對象的屬性的時候,forKey和forKeyPath,沒什麼區別:好比person有個name屬性 name咱們經過kvc修改name屬性的時候 這兩個方法並無區別編碼

 [p setValue:@"jack" forKey:@"name"];
 [p setValue:@"jack" forKeyPath:@"name"];

2.KeyPath方法中能夠利用.運算符, 就能夠一層一層往下查找對象的屬性,而key方法不行:atom

如果層次結構深一點的。好比person 有dog對象;dog有bone屬性時:spa

//這個是dog的屬性:
@class Bone;

@interface Dog : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) Bone *bone;
@end

//這個是bone的屬性:
@interface Bone : NSObject
@property (nonatomic, strong) NSString *type;
@end

咱們在經過kvc對bone屬性進行賦值時:.net

//forKeyPath能使用點語法,深層次的去尋找咱們須要的屬性
[p setValue:@"豬骨" forKeyPath:@"dog.bone.type"];
[p.dog setValue:@"豬骨" forKeyPath:@"bone.type"];

//這個方法也能夠替換成forKey方法
    [p.dog.bone setValue:@"豬骨" forKeyPath:@"type"];
關於KeyPath的更多功能 (好比對數組、字典或者NSSet的操做)

此外還有一些不太經常使用的方法也能夠了解下:設計

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值正確性�驗證的API,它能夠用來檢查set的值是否正確、爲不正確的值作一個替換值或者拒絕設置新值並返回錯誤緣由。

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操做的API,裏面還有一系列這樣的API,若是屬性是一個NSMutableArray,那麼能夠用這個方法來返回。

- (nullable id)valueForUndefinedKey:(NSString *)key;
//若是Key不存在,且沒有KVC沒法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常。

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個方法同樣,但這個方法是設值。

- (void)setNilValueForKey:(NSString *)key;
//若是你在SetValue方法時面給Value傳nil,則會調用這個方法

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,返回該組key對應的Value,再轉成字典返回,用於將Model轉到字典。

 

 

 

二、讀寫值順序:

賦值順序:3d

取值順序:

三、常見異常

1.KVC處理nil異常:

  一般狀況下,KVC不容許你要在調用setValue:屬性值 forKey:(或者keyPath)時對非對象傳遞一個nil的值。很簡單,由於值類型是不能爲nil的。若是你不當心傳了,KVC會調用setNilValueForKey:方法。這個方法默認是拋出異常,因此通常而言最好仍是重寫這個方法。
- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能將%@設成nil", key);
}

 

2.KVC處理UndefinedKey異常(也就是訪問的屬性名不存在):

  一般狀況下,KVC不容許你要在調用setValue:屬性值 forKey:(或者keyPath)時對不存在的key進行操做。否則,會報錯forUndefinedKey發生崩潰,

重寫forUndefinedKey方法避免崩潰。

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出現異常,該key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出現異常,該key不存在%@", key);
}

以上重寫的方法 都是寫在被讀寫對象的類對象的m文件

 

四、KVC+Runtime  字典轉模型

字典轉模型有不少方法,最直接的就是↓↓

 User *user = [[User alloc] init];
 user.name = dict[@"name"];
 user.icon = dict[@"icon"];
 ....

 

這種方法寫了大量重複代碼  不推薦;

也能夠經過KVC↓↓↓:

[User setValuesForKeysWithDictionary:dict];

KVC底層的實現其實是將字典進行遍歷,取出一個個Key  在模型中找同名的屬性  探後將鍵值對中的Value賦值到屬性中,這種方法的缺點就是字典中的Key必須在模型中都要找到對應的屬性,不然會報setValue: forUndefinedKey的錯誤,引起程序crasha(這個能夠經過重寫setValue: forUndefinedKey方法解決) 

/遍歷
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        // 這行代碼纔是真正給模型的屬性賦值
        [s setValue:dict[@"source"] forKey:@"source"];

}];

此外,如今比較經常使用的是MJExtension框架(涉及到KVC+Runtime),它與KVO模型轉字典流程不太同樣:

kvo是遍歷字典 根據key去模型中找屬性;

MJExtension是遍歷模型中的屬性,根據屬性名去字典中取對應的Value進行賦值.

也就是:

1.拿到模型的屬性名(注意屬性名和成員變量名的區別),和對應的數據類型.

2.用該屬性名做爲鍵去字典中尋找對應的值.

3.拿到值後將值轉換爲屬性對應的數據類型.

4.賦值.
//返回一個建立好的模型
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    //建立一個模型
    id objc = [[self alloc] init];
    int count = 0;
    /*
     方法:獲取成員變量列表
     參數一:class獲取哪一個類成員變量列表
     參數二:count成員變量總數
     */
    // 成員變量數組 指向數組第0個元素  這裏涉及到了Runtime
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍歷全部成員變量
    for (int i = 0; i < count; i++) {
        // 獲取成員變量
        Ivar ivar = ivarList[i];
        // 獲取成員變量名稱(將c轉爲oc)
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 成員變量名稱轉換key(將成員變量前邊的"_"截取掉)
        NSString *key = [ivarName substringFromIndex:1];

        // 從字典中取出對應value
        id value = dict[key];

        // 給模型中屬性賦值(底層會去找對應的屬性和值)
        [objc setValue:value forKey:key];
    }
    return objc;
}

上面介紹的只是簡單的一級轉換,更多關於MJExtension的介紹能夠看一下下面的相關資料↓↓↓

跟着MJExtension實現簡單的字典轉模型框架
[iOS]MJExtension 源碼閱讀
MJExtension源碼解析

 

五、經過kvc修改屬性值爲何會被kvo監聽到

  咱們在上篇文章中看到經過KVC修改屬性值也是能夠被KVO監聽到的,咱們的第一個想法就是由於kvc在修改屬性值的時候首先調用set方法,而遵照kvo的對象調用set方法其實是調用中間類對象重寫的set方法,在中間類重寫的set方法中進而對回調方法進行了調用。這個想法是正確的,正常狀況下確實是由於調用了中間類的set方法從而被監聽到。

  可是,根據實驗咱們也發現 若是經過kvc對某個對象的成員變量進行修改時  也能被kvo監聽到,成員變量是沒有set方法的。個人第一個想法是會不會是由於中間類的緣由。由於遵照kvo的對象由於isa是指向中間類的 而中間類會重寫set方法  但仔細想一想也不對  由於中間類並非隨便生成的  它是根據在最初的類對象基礎上重寫了某些方法而已  而原始的類對象自己就沒有實現set方法  由於成員變量沒有set方法 因此這個緣由是不成立的。那麼緣由就是由於kvc的內部實現中手動調用了KVO(即手動調用了willChangeValueForKey:didChangeValueForKey:方法)

  

咱們根據上面的賦值流程也知道 kvc是直接找到了變量直接進行賦值操做 那麼其內部原理應該是這樣的

在didChangeValueForKey方法內部會調用KVO的回調方法  從而實現監聽

 

  1 //
  2 //  ViewController.m
  3 //  kvcDemo
  4 //
  5 //  Created by 人 on 2018/10/30.
  6 //  Copyright © 2018 洪. All rights reserved.
  7 //
  8 /*
  9  KVC(Key-value coding)鍵值編碼,就是指iOS的開發中,能夠容許開發者經過Key名直接訪問對象的屬性,或者給對象的屬性賦值。而不須要調用明確的存取方法。這樣就能夠在運行時動態地訪問和修改對象的屬性。而不是在編譯時肯定,這也是iOS開發中的黑魔法之一。不少高級的iOS開發技巧都是基於KVC實現的。
 10  
 11  KVC的定義都是對NSObject的擴展來實現的,Objective-C中有個顯式的NSKeyValueCoding類別名,因此對於全部繼承了NSObject的類型,都能使用KVC(一些純Swift類和結構體是不支持KVC的,由於沒有繼承NSObject),下面是KVC最爲重要的四個方法:
 12      - (nullable id)valueForKey:(NSString *)key;                          //直接經過Key來取值
 13  
 14      - (void)setValue:(nullable id)value forKey:(NSString *)key;          //經過Key來設值
 15  
 16      - (nullable id)valueForKeyPath:(NSString *)keyPath;                  //經過KeyPath來取值
 17  
 18      - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //經過KeyPath來設值
 19  
 20 其餘的一些方法:
 21  
 22  + (BOOL)accessInstanceVariablesDirectly;
 23  //默認返回YES,表示若是沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設置成NO就不這樣搜索
 24  
 25  - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
 26  //KVC提供屬性值正確性�驗證的API,它能夠用來檢查set的值是否正確、爲不正確的值作一個替換值或者拒絕設置新值並返回錯誤緣由。
 27  
 28  - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
 29  //這是集合操做的API,裏面還有一系列這樣的API,若是屬性是一個NSMutableArray,那麼能夠用這個方法來返回。
 30  
 31  - (nullable id)valueForUndefinedKey:(NSString *)key;
 32  //若是Key不存在,且沒有KVC沒法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常。
 33  
 34  - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
 35  //和上一個方法同樣,但這個方法是設值。
 36  
 37  - (void)setNilValueForKey:(NSString *)key;
 38  //若是你在SetValue方法時面給Value傳nil,則會調用這個方法
 39  
 40  - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
 41  //輸入一組key,返回該組key對應的Value,再轉成字典返回,用於將Model轉到字典。
 42  
 43  */
 44 /**
 45 KVC設值
 46 
 47 KVC要設值,那麼就要對象中對應的key,KVC在內部是按什麼樣的順序來尋找key的。當調用setValue:屬性值 forKey:@」name「的代碼時,底層的執行機制以下:
 48 
 49 程序優先調用set<Key>:屬性值方法,代碼經過setter方法完成設置。注意,這裏的<key>是指成員變量名,首字母大小寫要符合KVC的命名規則,下同
 50 
 51 若是沒有找到setName:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,若是你重寫了該方法讓其返回NO的話,那麼在這一步KVC會執行setValue:forUndefinedKey:方法,不過通常開發者不會這麼作。因此KVC機制會搜索該類裏面有沒有名爲<key>的成員變量,不管該變量是在類接口處定義,仍是在類實現處定義,也不管用了什麼樣的訪問修飾符,只在存在以<key>命名的變量,KVC均可以對該成員變量賦值。
 52 
 53 若是該類即沒有set<key>:方法,也沒有_<key>成員變量,KVC機制會搜索_is<Key>的成員變量。
 54 
 55 和上面同樣,若是該類即沒有set<Key>:方法,也沒有_<key>和_is<Key>成員變量,KVC機制再會繼續搜索<key>和is<Key>的成員變量。再給它們賦值。
 56 
 57 若是上面列出的方法或者成員變量都不存在,系統將會執行該對象的setValue:forUndefinedKey:方法,默認是拋出異常。
 58 
 59 簡單來講就是若是沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員並進行賦值操做。
 60 
 61 若是開發者想讓這個類禁用KVC裏,那麼重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO便可,這樣的話若是KVC沒有找到set<Key>:屬性名時,會直接用setValue:forUndefinedKey:方法。
 62 **/
 63 
 64 
 65 #import "ViewController.h"
 66 #import "personClass.h"
 67 @interface ViewController ()
 68 
 69 @end
 70 
 71 @implementation ViewController
 72 + (BOOL)accessInstanceVariablesDirectly {
 73     return NO;
 74 }
 75 
 76 - (id)valueForUndefinedKey:(NSString *)key {
 77     NSLog(@"出現異常,該key不存在%@",key);
 78     return nil;
 79 }
 80 
 81 - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
 82     NSLog(@"出現異常,該key不存在%@", key);
 83 }
 84 
 85 - (void)viewDidLoad {
 86     [super viewDidLoad];
 87     personClass *cla = [[personClass alloc]init];
 88     cla.name = @"";
 89     
 90 //    對於類裏的私有屬性,Objective-C是沒法直接訪問的,可是KVC是能夠的。
 91     //這裏的address是personClass的私有屬性  因此沒法經過點語法來訪問
 92     [cla setValue:@"上海市" forKey:@"address"];
 93 
 94     //經過KVC取值的話若是取到的值是BOOL或者Int等值類型, 會將其包裝成一個NSNumber對象。
 95     NSLog(@"%@是%@的人,年齡是%@",cla.name,[cla valueForKey:@"address"],[cla valueForKey:@"age"]);
 96     //kvo是不容許傳空值的  傳空值的話會調用setNilValueForKey
 97     [cla setValue:nil forKey:@"age"];
 98     
 99     //kvc賦值的時候 不能直接將一個數值經過KVC賦值的,咱們須要把數據轉爲NSNumber和NSValue類型傳入
100     [cla setValue:[NSNumber numberWithInt:21] forKey:@"age"];
101     
102     
103     /*
104     KVC同時還提供了很複雜的函數,主要有下面這些:
105     簡單集合運算符共有@avg, @count , @max , @min ,@sum5種,分別是求平均數/求個數/求最大值/求最小值和求和
106      */
107     personClass *cla1 = [[personClass alloc]init];
108     [cla1 setValue:[NSNumber numberWithInt:21] forKey:@"age"];
109     
110     personClass *cla2 = [[personClass alloc]init];
111     [cla2 setValue:[NSNumber numberWithInt:1] forKey:@"age"];
112     
113     personClass *cla3 = [[personClass alloc]init];
114     [cla3 setValue:[NSNumber numberWithInt:21] forKey:@"age"];
115     
116     personClass *cla4 = [[personClass alloc]init];
117     [cla4 setValue:[NSNumber numberWithInt:42] forKey:@"age"];
118     
119     NSArray *claAry = @[cla1,cla2,cla3,cla4];
120     //KVC對於keyPath是搜索機制第一步就是分離key,用小數點.來分割key,而後再像普通key同樣按照先前介紹的順序搜索下去。
121     NSNumber* sum = [claAry valueForKeyPath:@"@sum.age"];
122     NSLog(@"sum:%f",sum.floatValue);
123     NSNumber* avg = [claAry valueForKeyPath:@"@avg.age"];
124     NSLog(@"avg:%f",avg.floatValue);
125     NSNumber* count = [claAry valueForKeyPath:@"@count"];
126     NSLog(@"count:%f",count.floatValue);
127     NSNumber* min = [claAry valueForKeyPath:@"@min.age"];
128     NSLog(@"min:%f",min.floatValue);
129     NSNumber* max = [claAry valueForKeyPath:@"@max.age"];
130     NSLog(@"max:%f",max.floatValue);
131     
132     /*
133     比集合運算符稍微複雜,能以數組的方式返回指定的內容,一共有兩種:
134     
135     @distinctUnionOfObjects
136     @unionOfObjects
137     它們的返回值都是NSArray,區別是前者返回的元素都是惟一的,是去重之後的結果;後者返回的元素是全集。
138      */
139     
140     //某一個數據中某個屬性的值集合去重後結果
141     NSArray* arrDistinct = [claAry valueForKeyPath:@"@distinctUnionOfObjects.age"];
142     for (NSNumber *age in arrDistinct) {
143         NSLog(@"%f",age.floatValue);
144     }
145     
146     ////某一個數據中某個屬性的值集合結果
147     NSArray* arrUnion = [claAry valueForKeyPath:@"@unionOfObjects.age"];
148     for (NSNumber *age in arrUnion) {
149         NSLog(@"%f",age.floatValue);
150     }
151     
152     //經過kvc進行字典轉模型
153     NSDictionary *dic = @{@"name":@"ming",@"age":@14,@"address":@"北京市",@"ds":@"dsad"};
154     /*
155     KVC裏面還有兩個關於NSDictionary的方法:
156     
157     - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
158     - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
159      
160     setValuesForKeysWithDictionary是用來修改Model中對應key的屬性。下面直接用代碼會更直觀一點:這個用來字典轉模型
161     */
162     personClass *p = [[personClass alloc]init];
163     //字典轉模型
164     [p setValuesForKeysWithDictionary:dic];
165     /*
166      KVC的實現模式是取出字典中的鍵值,去模型中找與之對應的屬性,那麼反之考慮,咱們能不能抓取模型中的屬性對象,去字典中找對應的鍵值呢?因此這就要考慮用到運行時機制runtime了。咱們先獲取到模型對象的屬性名,將他們加入到一個數組當中,而後遍歷數組,在遍歷過程當中給屬性對象賦值。這也是KVC和runtime用於實現字典轉模型的區別之一。
167      runtime是遍歷模型的屬性  若是這個屬性能夠從字典中找到對應的屬性值就賦值  找不到就判斷下一個模型屬性
168      kvc是遍歷字典的鍵值對  若是這個鍵值對的key是模型中的一個屬性  那麼就對模型進行賦值操做  若是這個key不是模型屬性那麼則判斷下一個鍵值對的key
169      */
170     NSLog(@"%@歲的%@住在%@",[p valueForKey:@"age"],p.name,[p valueForKey:@"address"]);
171     
172     
173      personClass *erp = [[personClass alloc]init];
174     erp.name = @"大名";
175     [erp setValue:@"提阿尼" forKey:@"address"];
176     [erp setValue:[NSNumber numberWithInt:12] forKey:@"age"];
177     
178     NSArray *mesAry = @[@"age",@"name"];
179 //    dictionaryWithValuesForKeys:是指輸入一組key,返回這組key對應的屬性,再組成一個字典。
180     NSDictionary *dicMes = [erp dictionaryWithValuesForKeys:mesAry];
181     NSLog(@"%@",dicMes);
182     /**
183      KVC的設計原理:
184      
185      [item setValue:@"value" forKey:@"property"]:
186      
187      1.首先去模型中查找有沒有setProperty,找到,直接調用賦值 [self setProperty:@"value"]
188      
189      2.去模型中查找有沒有property屬性,有,直接訪問屬性賦值  property = value
190      
191      3.去模型中查找有沒有_property屬性,有,直接訪問屬性賦值 _property = value
192      
193      4.找不到,就會直接報錯 setValue:forUndefinedKey:報找不到的錯誤
194      **/
195     
196   /*不少UI控件都由不少內部UI控件組合而成的,可是Apple度沒有提供這訪問這些控件的API,這樣咱們就沒法正常地訪問和修改這些控件的樣式。
197     而KVC在大多數狀況可下能夠解決這個問題。最經常使用的就是個性化UITextField中的placeHolderText了。
198     */
199 
200     
201 }
具體代碼

 

 

 

 KVC的更多功能
相關文章
相關標籤/搜索