1. KVC 用來做什么?
和對象的點語法類似,都是用來賦值和取值的。
2. 為什么要用 KVC?
通常,兩種情況下我們需要重新審視自己的賦值方式。
- 訪問私有屬性/變量。正常的情況下,我們根據(jù)設計原型,開開心心的搭建面,初始化各種控件,
UIView / UIButton / UILabel,然后設置其相應的屬性,以使界面滿足我們的需求。假如界面很簡單,效果不是很炫酷,那我們只需要通過設置子控件的各種屬性,調整背景顏色,切個圓角,改變透明度等等,就可以將界面搞定??墒?,有些效果,偏偏是通過蘋果暴露給我們的接口不能滿足的,比如,修改文字輸入框(UITextField)的占位文字的顏色,這個時候怎么辦?想要textField.placeholederLabel.textColor = [UIColor redColor]沒有這個屬性啊,怎么實現(xiàn)?另外還有一些只讀屬性,無法通過點語法正常賦值,怎么辦? - 請求服務器數(shù)據(jù)之后的處理。沒有KVC,我們如果想給某個屬性賦值,則需要通過
object.property = value; 這種方式進行賦值。試想一下,如果是一個對象的幾個屬性這樣設置還行得通,可是通常客戶端的數(shù)據(jù)都是從服務器端獲取,格式一般是 json/xml,我們需要通過字典轉模型,將字符串化的鍵值對轉換成對象的屬性值進行保存和使用,通常這種數(shù)據(jù)量是很大的,而且還是帶各種嵌套的,難道這個時候我們還要先創(chuàng)建各種對象,然后根據(jù)鍵值對的鍵,創(chuàng)建對應的屬性,之后再手動一個個model.property = value;保存嗎?肯定是不現(xiàn)實的。那怎么辦?
3. 有了 KVC 之后,上述問題怎么解決?
-
[textField setValue:[UIColor purpleColor] forKeyPath:@"_placeholderLabel.textColor"];當然,這里邊,找到正確的沒有暴露的接口也是個問題,以后再說吧,大致就是利用 Runtime ,給程序打斷點,然后將 UITextFiled 的所有變量(包括未暴露的私有變量)打印,之后根據(jù)名字猜測+嘗試+運氣通常就能找到我們需要修改的屬性了。
這里為什么要使用setValue: forKeyPath:而不是setValue: forKey:呢?因為_placeholederLabel是UITextField的屬性,而我們要設置的是_placeholederLabel的屬性,像這種有多層屬性嵌套的情況就需要使用setValue: forKeyPath:。
經過上邊一行簡單的代碼我們就可以很優(yōu)雅的實現(xiàn)修改占位文字的顏色問題了。這里需要說明的一點是:占位文字的顏色和大小還可以通過設置attributedPlaceholder屬性設置,也是比較方便的。代碼如下:
NSString *placeholder = @"我是占位文字";
NSDictionary *attr = @{NSForegroundColorAttributeName : [UIColor orangeColor],
NSFontAttributeName : [UIFont systemFontOfSize:15] };
textField.attributedPlaceholder = [[NSAttributedString alloc]initWithString:placeholder attributes:attr];
- 關于字典轉模型。這里先以很簡單的單層數(shù)據(jù)為例:

JSON 數(shù)據(jù)
然后是模型
TestModel的頭文件:
@interface TestModel : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *detail;
@property (copy, nonatomic) NSString *postscript;
- (instancetype)initWithDict:(NSDictionary *)dict;
@end
TestModel的實現(xiàn)文件:
@implementation TestModel
- (instancetype)initWithDict:(NSDictionary *)dict {
self = [super init];
if (self) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
@end
最后是ViewController控制器中的調用代碼:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 加載本地 json 數(shù)據(jù),正常應是請求服務器
[self loadData];
}
- (void)loadData {
NSString *jsonPath = [[NSBundle mainBundle] pathForResource:@"test.json" ofType:nil];
// 2. 解析數(shù)據(jù)
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:jsonPath] options:NSJSONReadingMutableContainers error:nil];
// 3. 轉換,只要屬性定義好,直接調用初始化方法即可
TestModel *model = [[TestModel alloc]initWithDict:jsonDict];
// 4. 打印驗證
NSLog(@"name - %@, detail - %@, postscript - %@", model.name, model.detail, model.postscript);
}
@end
打印結果驗證:

打印結果
通過上邊這個十分簡單的例子,我們就可以看到 KVC 在數(shù)據(jù)轉模型時的便捷了。
不過這么簡單的數(shù)據(jù)實際開發(fā)中很難遇到,這里這里只是演示一下,如果有多層嵌套的話,就需要在模型類中重寫
setValue: forKey方法,然后通過判斷key,對嵌套的字段再進行轉換。

嵌套的 JSON 數(shù)據(jù)
最外層模型
TestModel的頭文件:
@interface TestModel : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *detail;
@property (copy, nonatomic) NSString *postscript;
// 多了一個嵌套的模型屬性
@property (strong, nonatomic) NestedTestModel *nestedModel;
- (instancetype)initWithDict:(NSDictionary *)dict;
@end
最外層模型TestModel的實現(xiàn)文件:
@implementation TestModel
- (instancetype)initWithDict:(NSDictionary *)dict {
self = [super init];
if (self) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
- (void)setValue:(id)value forKey:(NSString *)key {
// 單獨處理嵌套的 key
if ([key isEqualToString:@"nested"]) {
self.nestedModel = [[NestedTestModel alloc]initWithDict:value];
} else {
// 別忘了調用父類的方法,否則其他不需要單獨處理的屬性就不解析了
[super setValue:value forKey:key];
}
}
@end
控制器的代碼:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 加載本地 json 數(shù)據(jù),正常應是請求服務器
[self loadData];
}
- (void)loadData {
NSString *jsonPath = [[NSBundle mainBundle] pathForResource:@"test.json" ofType:nil];
// 2. 解析數(shù)據(jù)
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:jsonPath] options:NSJSONReadingMutableContainers error:nil];
// 3. 轉換,只要屬性定義好,直接調用初始化方法即可
TestModel *model = [[TestModel alloc]initWithDict:jsonDict];
// 4. 打印驗證
NSLog(@"name - %@, detail - %@, postscript - %@, nestedName - %@, nestedDetail - %@, nestedPostscript - %@, ", model.name, model.detail, model.postscript, model.nestedModel.nestedName, model.nestedModel.nestedDetail, model.nestedModel.nestedPostscript);
}
@end

打印結果
這里嵌套內部的模型NestedTestModel基本跟單層 JSON 中的模型的代碼一致,也就是打印驗證那一點,也就不再貼代碼了。文章最底有git地址。
其實,例子中的數(shù)據(jù)解析跟實際的復雜度也差好多,這時候我們往往會使用第三方框架,簡單方便。這里推薦一個比較小眾的第三方框架:NSObject-ObjectMap,可以看一下它的源文件,寫的很規(guī)整,就一個分類,可以實現(xiàn) json/xml 到對象的轉換,當然集合類的屬性也可以自動轉換(需要多寫一點代碼),里邊有好多方法用都牽扯到了 Runtime 和 OC 中的反射機制。
4. KVC 這么牛掰,系統(tǒng)是怎么實現(xiàn)的呢?
當我們調用setValue: forKey:testKey方法時,系統(tǒng)會通過以下查找順序進行賦值:

setValue:forKey:順序官方文檔
由上圖可以看出,在使用 KVC 賦值時的查找順序依次為:
setTestKey方法 --> _testKey --> _isTestKey --> testKey --> isTestKey。
后邊尋找實例變量的過程,有一個大的前提:對象的accessInstanceVariablesDirectly方法返回YES,當然默認情況下這個方法就是返回YES的,所以并不需要我們擔心。如果都找不到,那就調用 setValue:forUndefinedKey直接拋出異常。有時候如果我們不想讓程序因為這個原因拋出異常,可以在類的實現(xiàn)文件中重寫該方法即可。