KVC
什么是KVC
- KVC是Key-Value-Coding 的簡稱悲立。
- KVC是一種可以直接通過字符串的名字key來訪問類屬性的機制,而不是通過調(diào)用setter叶摄、getter方法去訪問递递。
- 我們可以通過在運行時動態(tài)的訪問和修改對象的屬性算灸。而不是在編譯時確定,KVC是iOS開發(fā)中的黑魔法之一锣笨。
KVC主要方法
KVC 定義了一種按名稱訪問對象屬性的機制蝌矛,支持這種訪問的主要方法是:
- 設(shè)置值
// value的值為OC對象,如果是基本數(shù)據(jù)類型要包裝成NSNumber
- (void)setValue:(id)value forKey:(NSString *)key;
// keyPath鍵路徑错英,類型為xx.xx
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
// 它的默認實現(xiàn)是拋出異常入撒,可以重寫這個函數(shù)做錯誤處理。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
- 獲取值
// value的值為OC對象椭岩,- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
// 如果Key不存在茅逮,且沒有KVC無法搜索到任何和Key有關(guān)的字段或者屬性,則會調(diào)用這個方法判哥,默認是拋出異常
- (id)valueForUndefinedKey:(NSString *)key;
NSKeyValueCoding 類別中還有其他的一些方法:
// 允許直接訪問實例變量献雅,默認返回YES。如果某個類重寫了這個方法塌计,且返回NO挺身,則KVC不可以訪問該類。
+ (BOOL)accessInstanceVariablesDirectly;
// 這是集合操作的API锌仅,里面還有一系列這樣的API瞒渠,如果屬性是一個NSMutableArray良蒸,那么可以用這個方法來返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 如果你在setValue方法時面給Value傳nil,則會調(diào)用這個方法
- (void)setNilValueForKey:(NSString *)key;
// 輸入一組key伍玖,返回該組key對應(yīng)的Value嫩痰,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典窍箍。
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
// KVC提供屬性值確認的API串纺,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設(shè)置新值并返回錯誤原因椰棘。
- (BOOL)validateValue:(id)ioValue forKey:(NSString *)inKey error:(NSError)outError;
舉個栗子:
@interface Teacher : NSObject
{
@private
int _age;
}
@property (nonatomic, strong, readonly) NSString *name;
@property (nonatomic, assign, getter = isMale) BOOL male;
- (void)log;
@end
這個類有私有private 變量和只讀readonly變量纺棺,如果用一般的setter和getter,在類外部是不能訪問到私有變量的邪狞,不能設(shè)值給只讀變量祷蝌,那是不是就拿它沒辦法了呢?
然而KVC可以做到,就是這么神奇帆卓。
Teacher *teacher = [Teacher new];
[teacher log];
// 設(shè)置 readonly value
[teacher setValue:@"Jack" forKey:@"name"];
// teacher.name = @"Jack";
// 設(shè)置 private value
[teacher setValue:@24 forKey:@"age"];
// teacher.age = 24;
[teacher setValue:@1 forKey:@"male"];
[teacher log];
// 獲取 readonly value
NSLog(@"name: %@", [teacher valueForKey:@"_name"]);
// 獲取 private value
NSLog(@"age: %d", [[teacher valueForKey:@"_age"] intValue]);
NSLog(@"male: %d", [[teacher valueForKey:@"isMale"] boolValue]);
KVC實現(xiàn)細節(jié)
- (void)setValue:(id)value forKey:(NSString *)key;
- 首先搜索 setter 方法巨朦,有就直接賦值。
- 如果上面的 setter 方法沒有找到剑令,再檢查類方法
+ (BOOL)accessInstanceVariablesDirectly
- 返回 NO糊啡,則執(zhí)行
setValue:forUNdefinedKey:
- 返回 YES,則按<key>吁津,<isKey>棚蓄,<key>,<isKey>的順序搜索成員名碍脏。
- 還沒有找到的話梭依,就調(diào)用
setValue:forUndefinedKey:
- (id)valueForKey:(NSString *)key;
- 首先查找 getter 方法,找到直接調(diào)用典尾。如果是 bool睛挚、int、float 等基本數(shù)據(jù)類型急黎,會做 NSNumber 的轉(zhuǎn)換。
- 如果沒查到侧到,再檢查類方法
+ (BOOL)accessInstanceVariablesDirectly
- 返回 NO勃教,則執(zhí)行
valueForUNdefinedKey:
- 返回 YES,則按_<key>,_is<Key>,<key>,is<Key>的順序搜索成員名匠抗。
- 返回 NO勃教,則執(zhí)行
- 還沒有找到的話故源,調(diào)用
valueForUndefinedKey:
KVC與點語法比較
用 KVC 訪問屬性和用點語法訪問屬性的區(qū)別:
- 用點語法編譯器會做預(yù)編譯檢查,訪問不存在的屬性編譯器會報錯汞贸,但是用 KVC 方式編譯器無法做檢查绳军,如果有錯誤只能運行的時候才能發(fā)現(xiàn)(crash)印机。
- 相比點語法用 KVC 方式 KVC 的效率會稍低一點,但是靈活门驾,可以在程序運行時決定訪問哪些屬性射赛。
- 用 KVC 可以訪問對象的私有成員變量。
KVC 應(yīng)用
字典轉(zhuǎn)模型
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
集合類操作符
獲取集合類的 count奶是,max楣责,min,avg聂沙,svm秆麸。確保操作的屬性為數(shù)字類型,否則會報錯及汉。
NSMutableArray *bookList = [NSMutableArray array];
for (int i = 0; i <= 20; i++) {
Book *book = [[Book alloc] initWithName:[NSString stringWithFormat:@"book%d",i] price:i*10];
[bookList addObject:book];
}
Student *student = [[Student alloc] initWithBookList:bookList];
Teacher *teacher = [[Teacher alloc] initWithStudent:student];
// KVC獲取數(shù)組
for (Book *book in [student valueForKey:@"bookList"]) {
NSLog(@"bookName : %@ \t price : %f",book.name,book.price);
}
NSLog(@"All book name : %@",[teacher valueForKeyPath:@"student.bookList.name"]);
NSLog(@"All book name : %@",[student valueForKeyPath:@"bookList.name"]);
NSLog(@"All book price : %@",[student valueForKeyPath:@"bookList.price"]);
// 計算(確保操作的屬性為數(shù)字類型沮趣,否則會報錯。) 五種集合運算符
NSLog(@"count of book price : %@",[student valueForKeyPath:@"bookList.@count.price"]);
NSLog(@"min of book price : %@",[student valueForKeyPath:@"bookList.@min.price"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.@max.price"]);
NSLog(@"sum of book price : %@",[student valueForKeyPath:@"bookList.@sum.price"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.@avg.price"]);
打印結(jié)果如下:
All book price : (
0,
10,
20,
30,
40,
50,
60,
70,
80,
90,
100
)
2017-01-18 20:45:26.640887 KVC-Demo[58294:5509308] count of book price : 11
2017-01-18 20:45:26.640956 KVC-Demo[58294:5509308] min of book price : 0
2017-01-18 20:45:26.641039 KVC-Demo[58294:5509308] avg of book price : 100
2017-01-18 20:45:26.641220 KVC-Demo[58294:5509308] sum of book price : 550
2017-01-18 20:45:26.641300 KVC-Demo[58294:5509308] avg of book price : 50
修改私有屬性
- 修改 TextField 的 placeholder:
[_textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[_textField setValue:[UIFont systemFontOfSize:14] forKeyPath:@“_placeholderLabel.font"];
- 修改 UIPageControl 的圖片:
[_pageControl setValue:[UIImage imageNamed:@"selected"] forKeyPath:@"_currentPageImage"];
[_pageControl setValue:[UIImage imageNamed:@"unselected"] forKeyPath:@"_pageImage"];
KVC 總結(jié)
鍵值編碼是一種間接訪問對象的屬性使用字符串來標識屬性坷随,而不是通過調(diào)用存取方法直接或通過實例變量訪問的機制房铭,非對象類型的變量將被自動封裝或者解封成對象,很多情況下會簡化程序代碼甸箱。
優(yōu)點:
- 不需要通過 setter育叁、getter 方法去訪問對象的屬性,可以訪問對象的私有屬性芍殖。
- 可以輕松處理集合類(NSArray)豪嗽。
缺點:
- 一旦使用 KVC 你的編譯器無法檢查出錯誤,即不會對設(shè)置的鍵豌骏、鍵值路徑進行錯誤檢查龟梦。
- 執(zhí)行效率要低于 setter 和 getter 方法。因為使用 KVC 鍵值編碼窃躲,它必須先解析字符串计贰,然后在設(shè)置或者訪問對象的實例變量。
- 使用 KVC 會破壞類的封裝性蒂窒。
KVO
- KVO 是 Key-Value-Observing 的簡稱躁倒。
- KVO 是一個觀察者模式。觀察一個對象的屬性洒琢,注冊一個指定的路徑秧秉,若這個對象的的屬性被修改,則 KVO 會自動通知觀察者衰抑。
- 更通俗的話來說就是任何對象都允許觀察其他對象的屬性象迎,并且可以接收其他對象狀態(tài)變化的通知。
KVO基本使用
1.// 注冊觀察者呛踊,實施監(jiān)聽砾淌;
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
2.// 回調(diào)方法啦撮,在這里處理屬性發(fā)生的變化;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
3.// 移除觀察者汪厨;
[self removeObserver:self forKeyPath:@“age"];
KVO 在 Apple 中的 API 文檔如下:
Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …
Apple 使用了 isa 攪拌技術(shù)(isa-swizzling)來實現(xiàn)的 KVO 赃春。當(dāng)一個觀察者注冊對象的一個屬性 isa 觀察對象的指針被修改,指著一個中間類而不是在真正的類骄崩。
isa 指針的作用:每個對象都有 isa 指針聘鳞,指向該對象的類,它告訴 runtime 系統(tǒng)這個對象的類是什么要拂。
舉個栗子:
_person = [[Person alloc] init];
/**
* 添加觀察者
*
* @param observer 觀察者
* @param keyPath 被觀察的屬性名稱
* @param options 觀察屬性的新值抠璃、舊值等的一些配置(枚舉值,可以根據(jù)需要設(shè)置脱惰,例如這里可以使用兩項)
* @param context 上下文搏嗡,可以為nil。
*/
[_person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
/**
* KVO回調(diào)方法
*
* @param keyPath 被修改的屬性
* @param object 被修改的屬性所屬對象
* @param change 屬性改變情況(新舊值)
* @param context context傳過來的值
*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
{
NSLog(@"%@對象的%@屬性改變了:%@",object,keyPath,change);
}
/**
* 移除觀察者
*/
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"age"];
}
KVO實現(xiàn)原理
當(dāng)某個類的對象第一次被觀察時拉一,系統(tǒng)就會在運行期動態(tài)地創(chuàng)建該類的一個派生類采盒,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法。
派生類在被重寫的 setter 方法實現(xiàn)真正的通知機制蔚润,就如前面手動實現(xiàn)鍵值觀察那樣磅氨。這么做是基于設(shè)置屬性會調(diào)用 setter 方法,而通過重寫就獲得了 KVO 需要的通知機制嫡纠。當(dāng)然前提是要通過遵循 KVO 的屬性設(shè)置方式來變更屬性值烦租,如果僅是直接修改屬性對應(yīng)的成員變量,是無法實現(xiàn) KVO 的除盏。
同時派生類還重寫了 class 方法以“欺騙”外部調(diào)用者它就是起初的那個類叉橱。然后系統(tǒng)將這個對象的 isa 指針指向這個新誕生的派生類,因此這個對象就成為該派生類的對象了者蠕,因而在該對象上對 setter 的調(diào)用就會調(diào)用重寫的 setter窃祝,從而激活鍵值通知機制。此外踱侣,派生類還重寫了 dealloc 方法來釋放資源粪小。
派生類 NSKVONotifying_Person 剖析:
在這個過程,被觀察對象的 isa 指針從指向原來的 Person 類抡句,被 KVO 機制修改為指向系統(tǒng)新創(chuàng)建的子類 NSKVONotifying_Person 類探膊,來實現(xiàn)當(dāng)前類屬性值改變的監(jiān)聽。
所以當(dāng)我們從應(yīng)用層面上看來玉转,完全沒有意識到有新的類出現(xiàn),這是系統(tǒng)“隱瞞”了對 KVO 的底層實現(xiàn)過程殴蹄,讓我們誤以為還是原來的類究抓。但是此時如果我們創(chuàng)建一個新的名為 NSKVONotifying_Person 的類()猾担,就會發(fā)現(xiàn)系統(tǒng)運行到注冊 KVO 的那段代碼時程序就崩潰,因為系統(tǒng)在注冊監(jiān)聽的時候動態(tài)創(chuàng)建了名為 NSKVONotifying_Person 的中間類刺下,并指向這個中間類了绑嘹。
因而在該對象上對 setter 的調(diào)用就會調(diào)用已重寫的 setter,從而激活鍵值通知機制橘茉。這也是 KVO 回調(diào)機制工腋,為什么都俗稱 KVO 技術(shù)為黑魔法的原因之一吧:內(nèi)部神秘、外觀簡潔畅卓。
子類 setter 方法剖析:
KVO 在調(diào)用存取方法之前總是調(diào)用 willChangeValueForKey:擅腰,通知系統(tǒng)該 keyPath 的屬性值即將變更。
當(dāng)改變發(fā)生后翁潘,didChangeValueForKey: 被調(diào)用趁冈,通知系統(tǒng)該 keyPath 的屬性值已經(jīng)變更。
之后拜马,observeValueForKey:ofObject:change:context: 也會被調(diào)用渗勘。
重寫觀察屬性的 setter 方法這種方式是在運行時而不是編譯時實現(xiàn)的。
KVO 為子類的觀察者屬性重寫調(diào)用存取方法的工作原理在代碼中相當(dāng)于:
- (void)setName:(NSString *)newName
{
[self willChangeValueForKey:@"name"]; // KVO在調(diào)用存取方法之前總調(diào)用
[super setValue:newName forKey:@"name"]; // 調(diào)用父類的存取方法
[self didChangeValueForKey:@"name"]; // KVO在調(diào)用存取方法之后總調(diào)用
}
總結(jié):
KVO的本質(zhì)就是監(jiān)聽對象的屬性進行賦值的時候有沒有調(diào)用setter方法
- 系統(tǒng)會動態(tài)創(chuàng)建一個繼承于 Person 的 NSKVONotifying_Person
- person 的 isa 指針指向的類 Person 變成 NSKVONotifying_Person俩莽,所以接下來的 person.age = newAge 的時候旺坠,他調(diào)用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子類)的 setter 方法
- 重寫NSKVONotifying_Person的setter方法:[super setName:newName]
- 通知觀察者告訴屬性改變扮超。
KVO實現(xiàn)實踐
MJReresh就是監(jiān)聽ScrollView 的 contentOffSet等屬性實現(xiàn)的
#pragma mark - KVO監(jiān)聽
- (void)addObservers
{
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil];
[self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil];
self.pan = self.scrollView.panGestureRecognizer;
[self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil];
}
KVO 總結(jié)
KVO 是一個對象能觀察另一個對象屬性的值取刃,KVO 適合任何對象監(jiān)聽另一個對象的改變,這是一個對象與另外一個對象保持同步的一種方法瞒津。KVO 只能對屬性做出反應(yīng)蝉衣,不會用來對方法或者動作做出反應(yīng)。
優(yōu)點:
- 提供一個簡單的方法來實現(xiàn)兩個對象的同步巷蚪。
- 能夠提供觀察的屬性的新值和舊值病毡。
- 每一次屬性值改變都是自動發(fā)送通知,不需要開發(fā)者手動實現(xiàn)屁柏。
- 用 keypath 來觀察屬性啦膜,因此也可以觀察嵌套對象。
缺點:
- 觀察的屬性必須使用字符串來定義淌喻,因此編譯器不會出現(xiàn)警告和檢查
- 只能重寫回調(diào)方法來后去通知僧家,不能自定義 selector。當(dāng)觀察多個對象的屬性時就要寫"if"語句裸删,來判斷當(dāng)前的回調(diào)屬于哪個對象的屬性的回調(diào)八拱。