寫在前面
之前寫事岀無常必有妖-iOS捉妖記之(Runtime)時說好要寫一篇關於runtime的詳細介紹的。看到這個標題關注了我的小夥伴們放下手裡的西瓜刀,小的並沒有棄坑,只是有簡友評論希望多結合例項來介紹runtime所以這幾天正苦苦搜尋通俗易懂又比較有價值的例項,所以先來水一篇KVC相關的文章。
文章最後會雞賊的教大家一個KVC實用技巧,打造一個萬能容器物件!如果你們公司的後臺返回的引數飄忽不定而老專案中接收後臺返回資料的又是用的一個通用模型時就可以派上用場了!
看完本篇之後你將獲得:
- 瞭解什麼是kvc
- kvc特性
- 掌握用kvc實現一個萬能容器物件的方法
- 歸納kvc的優缺點
引言
kvc是每個iOS開發者在學習obj-C時都學過的特性,但是由於obj-C學起來並不難,所以很多初學者把大部分時間都放在熟悉cocoa框架以及對iOS開發相關的API掌握上。其實在專案中巧用kvc可以大大提高開發效率減少程式碼量,下面我們進入正題。
定義
KVC(Key-Value-Coding)意思是鍵值編碼。在iOS中,提供了一種方法通過使用屬性的名稱(也就是Key)來間接訪問物件的屬性方法。
說的有點兒拗口,實際上就是通過類定義我們可以看到類的各種屬性,那麼使用屬性的名稱我們就能訪問到類例項化後的物件的這個屬性值。
KVC特性
1.無setter/getter方法也可以直接去找對應名稱的變數操作
KVC中最常見的也是最基本的呼叫方法:
1 2 3 4 5 |
//呼叫這個方法找到與key匹配的變數並把value賦值給它 - (void)setValue:(nullable id)value forKey:(NSString *)key; //呼叫這個方法找到與key匹配的變數並返回 - (nullable id)valueForKey:(NSString *)key; |
那麼上面的方法對比直接呼叫getter/setter方法時有什麼區別呢?
- 呼叫上面的方法其實預設會呼叫接收訊息物件的getter/setter方法來對與key鍵匹配的屬性進行讀/寫操作
- 如果接受訊息的物件並沒有實現相應的getter/setter方法的話,會直接訪問物件中的匹配變數作相應操作(包括私有變數)
第一點印證例項:
1 2 3 4 5 |
@interface LXObj : NSObject { NSString *ivar; } @end |
在.m檔案中的程式碼空空如也:
1 2 |
@implementation LXObj @end |
然後執行以下程式碼:
1 2 |
LXObj *obj = [[LXObj alloc] init]; obj.ivar = @"i am a ivar"; |
發現靜態解析器編譯不過去……
如果按照以下程式碼去操作ivar變數即可使程式正常執行:
1 2 3 |
LXObj *obj = [[LXObj alloc] init]; [obj setValue:@"i am a ivar" forKey:@"ivar"]; NSLog(@"%@", [obj valueForKey:@"ivar"]); |
執行結果如下:
那麼我們再來看一下KVC訪問下的所謂obj-C私有變數,首先在類中新增私有變數:
1 2 3 |
@interface LXObj : NSObject { <a href='http://www.jobbole.com/members/kaishu6296'>@private</a> NSString *privateIvar; } |
或者在類擴充套件中宣告變數:
1 2 3 |
@interface LXObj () @property (nonatomic, copy) NSString *privateIvar; @end |
為了解釋的更明白,讓所有人(包括一些初學obj-C的小夥伴)看懂,先用普通的方式訪問:
都編譯不過去,靜態分析器就幫我們找到了錯誤,那麼我們換KVC方式訪問:
1 2 3 |
LXObj *obj = [[LXObj alloc] init]; [obj setValue:@"i am a private ivar" forKey:@"privateIvar"]; NSLog(@"%@", [obj valueForKey:@"privateIvar"]); |
然後執行,發現可以正常執行:
看到這,你會得出或者印證你以前學習obj-C時書上提到的:obj-C實際上並不存在真正的私有變數,因為只要知道變數名稱就可以訪問且操作這個變數。
你不要萌萌的瞪著眼睛問我:我們為啥還要遵守規則把變數寫在.m內的類擴充套件中呢?
因為當你的app提交到appstore中被人下載得到下載包後,別人反編譯分分鐘就能看到你的專案標頭檔案,類名和方法(其實我不會告訴你.m檔案也能看到,只不過反編譯後的.m依舊是一坨坨的程式碼不像.h那樣容易看懂。不用擔心你還可以混淆啊,你不要問我obj-C怎麼混淆,再說下去就跑題了……額,好吧我承認對obj-C混淆也不太懂>_
2.使用KVC會自動開/封箱
如果你想設定一個標準量,在呼叫- (void)setValue:(nullable id)value forKey:(NSString *)key
方法之前需要將它們封箱:
先給LXObj類新增一個floatNum屬性:
1 |
@property (nonatomic, assign) float floatNum; |
然後執行下面程式碼:
1 2 |
LXObj *obj = [[LXObj alloc] init]; [obj setValue:[NSNumber numberWithFloat:0.1] forKey:@"floatNum"]; |
這時,- (void)setValue:(nullable id)value forKey:(NSString *)key
方法會先開箱取出該值,再呼叫- (void)setFloatNum:
方法或者直接更改floatNum例項變數,反之:
1 2 |
LXObj *obj = [[LXObj alloc] init]; NSLog(@"%@", [obj valueForKey:@"floatNum"]); |
程式碼中[obj valueForKey:@"floatNum"]
方法會先取出floatNum屬性的值並封箱列印出來。
3.鍵路徑
當類中包含其他類型別的屬性時,可以直接使用鍵路徑來操作這個屬性內部的變數。
先定義一個LXSubObj類,其中包含一個屬性subIvar:
1 2 3 |
@interface LXSubObj : NSObject @property (nonatomic, copy) NSString *subIvar; @end |
然後往LXObj中新增一個LXSubObj型別的屬性:
1 2 3 4 |
@class LXSubObj; @interface LXObj : NSObject @property (nonatomic, strong) LXSubObj *subObj; @end |
此時要操作LXObj物件中的subObj屬性的subIvar可以使用
1 2 3 4 |
LXObj *obj = [[LXObj alloc] init]; obj.subObj = [[LXSubObj alloc] init]; [obj setValue:@"operate subObj's subIvar" forKeyPath:@"subObj.subIvar"]; NSLog(@"%@", [obj valueForKeyPath:@"subObj.subIvar"]); |
執行結果:
Ps:需要注意,如果LXObj下有一組型別為LXSubObj的陣列作為屬性,那麼NSArray實現valueForKeyPath:
的方法是迴圈遍歷它的內容並向每個物件傳送資訊。也就是說NSArray會向每個在自身之中的LXSubObj物件傳送引數以subIvar作為鍵路徑的valueForKeyPath:
訊息。
3.批處理
這個由於各位在初學iOS階段可能就用到過,而且平時開發也會使用到,就一筆帶過吧:
1 2 3 4 5 |
// 根據所給字典一一對應的設定接收訊息物件內的屬性值 - (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues; // 根據陣列keys一一對應的從接收訊息物件內取出對應的值生成字典返回 - (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys; |
4.快速運算
通過快速運算特性可以節約開發成本,簡化程式碼,先把上面LXObj內部的subObj改為NSArray型別(這裡用到了泛型語法):
1 2 3 4 |
@class LXSubObj; @interface LXObj : NSObject @property (nonatomic, strong) NSArray *subObjs; @end |
.m中初始化陣列subObjs:
1 2 3 4 5 6 7 8 9 10 11 |
@implementation LXObj - (NSArray *)subObjs { if (!_subObjs) { LXSubObj *subObj1 = [[LXSubObj alloc] init]; LXSubObj *subObj2 = [[LXSubObj alloc] init]; _subObjs = @[subObj1, subObj2]; } return _subObjs; } @end |
然後執行下面的程式碼:
1 2 |
LXObj *obj = [[LXObj alloc] init]; NSLog(@"%@", [obj valueForKeyPath:@"subObjs.@count"]); |
執行結果:
類似@count的例子還有很多:
用kvc實現一個萬能容器物件的方法
終於到的正題,這裡我們為了形象一些,拿車子來舉例。下面的LXCar類沒有任何屬性但是理論上可以存任意數量的任意型別的屬性。
LXCar.h檔案內程式碼:
1 2 3 |
@interface LXCar : NSObject // 父老鄉親們,看清楚,這容器就是空的!!! @end |
LXCar.m內程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@interface LXCar () // 用來放置屬性鍵值對的字典 @property (nonatomic, strong) NSMutableDictionary *mPropertiesDict; @end @implementation LXCar // 沒有對應key的setter方法且沒有找到對應key的屬性時呼叫 - (void)setValue:(id)value forUndefinedKey:(NSString *)key { if (!key || [key isEqualToString:@""]) return; if (!_mPropertiesDict) { _mPropertiesDict = [NSMutableDictionary dictionary]; } [_mPropertiesDict setValue:value forKey:key]; } // 沒有對應key的getter方法且沒有找到對應key的屬性時呼叫 - (id)valueForUndefinedKey:(NSString *)key { if (!key || [key isEqualToString:@""]) return nil; return [_mPropertiesDict valueForKey:key]; } @end |
然後執行程式碼:
1 2 3 4 5 6 |
LXCar *car = [[LXCar alloc] init]; [car setValue:@"保時捷 卡宴 3.0T 鉑金版" forKey:@"name"]; [car setValue:@"保時捷" forKey:@"brand"]; [car setValue:@"卡宴" forKey:@"categroy"]; NSLog(@"car name = %<a href='http://www.jobbole.com/members/Famous_god'>@,</a> car brand = %<a href='http://www.jobbole.com/members/Famous_god'>@,</a> categroy = %@", [car valueForKey:@"name"], [car valueForKey:@"brand"], [car valueForKey:@"categroy"]); |
下面是執行結果,你會發現原本空空的LXCar類被我們利用KVC特性打造成了萬能的容器,可以放下原本沒有的name/brand/categroy屬性:
其實相信只要是耐著性子從頭看到現在的小夥伴們都能看的懂上面的程式碼:利用接收物件既沒有相應的setter/getter方法又無物件屬性時呼叫- (void)setValue:(id)value forUndefinedKey:(NSString *)key
以及- (id)valueForUndefinedKey:(NSString *)key
兩個方法的特性,給物件加入了一個可變字典作為填充屬性的區域實現這樣一個萬能容器。
歸納kvc的優缺點
看了文章上面所講的,你可能已經愛上KVC了。但是請清醒一下,萬物都有兩面性,如果濫用KVC的話也不是什麼好事:
- KVC需要解析字串來計算你所需要的答案,因此速度比較慢
- 編輯器無法對KVC進行錯誤檢查,當你的key鍵輸入錯誤時會引起執行時錯誤
寫在最後
就像本文前面說的一樣,這種萬能容器物件可以用在後臺介面平時還算規範但是偶爾會多返回一些出參的情況。所以不論是runtime還是KVC,是底層知識還是語言特性,一定要學以致用。畢竟空懂一腔理論知識卻沒有解決問題的能力學再多的東西也沒有用……