KVC(Key-value coding):鍵值對編碼,也就是咱們能夠經過變量的名稱來讀取或者修改它的值,而不須要調用明確的存取方法。這樣就能夠在運行時動態地訪問和修改對象的屬性。而不是在編譯時肯定。對於類裏的私有屬性,Objective-C是沒法直接訪問的,可是KVC是能夠的。數組
做用:框架
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"];
此外還有一些不太經常使用的方法也能夠了解下:設計
- (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異常:
- (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文件
字典轉模型有不少方法,最直接的就是↓↓
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的介紹能夠看一下下面的相關資料↓↓↓
咱們在上篇文章中看到經過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 }