引子
KVO:即Key-Value-Observer,鍵值觀測模式肋乍,它是一種允許當(dāng)某些對象的特定屬性值改變時鹅颊,及時通知給對象的觀察者(其他對象)的機制。
觀察者的注冊和移除
KVO的大致流程包括:給要監(jiān)聽的屬性所屬的類添加觀察者墓造;接收到屬性改變的通知后進行處理堪伍;處理完之后接觸觀察者三大步驟。流程很簡單觅闽,就像要把大象裝進冰箱總共需幾步類似杠娱。其中,一對一代表對非集合類的屬性監(jiān)聽谱煤,一對多代表對集合類的屬性監(jiān)聽摊求。
注冊
注冊方法:
[person addObserver:observer
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:NULL];
各個參數(shù)的意義很明了。分別依次是:被觀察類的實例對象刘离,觀察者類的實例對象室叉,被觀察的屬性名稱,觀察選項硫惕,額外參數(shù)茧痕。
注冊選項
注冊選項包括四個,它們的名字和效果依次是:
- NSKeyValueObservingOptionNew:通知觀察者屬性發(fā)生變化時的新值恼除;
- NSKeyValueObservingOptionOld:通知觀察者屬性發(fā)生變化時之前的舊值踪旷;
- NSKeyValueObservingOptionInitial:在注冊觀察者方法(
addObserver:observer
)未返回時就會開始發(fā)送通知,因為被觀察者的初始化(initial value)對于觀察者來說也是新變化的值豁辉; - NSKeyValueObservingOptionPrior:發(fā)送兩條通知令野,也就是當(dāng)屬性值即將要發(fā)生變化時,即下邊要說的
willChangeValueForKey
觸發(fā)時間相對應(yīng)徽级,預(yù)先發(fā)送給觀察者一條通知气破,待屬性值改變之后跟上述三個選項一樣還會發(fā)出通知。
通知的處理
觀察者收到通知后餐抢,需要通過特定的方法進行處理现使,樣例代碼:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
{
NSLog(@"%@", change);
if ([keyPath isEqualToString:@"name"])
{
//通知事件的處理
NSLog(@"%@的名字發(fā)生了變化低匙!%@", object, change);
}
}
這個方法一定要在觀察者的類中進行重寫。
通知信息
通知處理方法中的change:(NSDictionary<NSString *,id> *)change
是一個字典類型的對象碳锈,包含了此次變化的信息顽冶,例如NSKeyValueObservingOptionNew
選項下,一對一的屬性發(fā)生變化時接收到的變化信息如下:
{
kind = 1;
new = hexintao;
}
第一條的kind
是NSKeyValueChange
類型的枚舉值售碳,它有如下四個定義:
- NSKeyValueChangeSetting:設(shè)置新值渗稍,被監(jiān)聽的是一對一的屬性或者一對多的屬性;
- NSKeyValueChangeInsertion:一對多的屬性新插入了一個對象团滥;
- NSKeyValueChangeRemoval:一對多的屬性移除了一個對象竿屹;
- NSKeyValueChangeReplacement:一對多的屬性替換了其中的某個對象。
第二條則會根據(jù)NSKeyValueObservingOptions
觀察選項灸姊、一對一或者一對多的屬性不同而不同拱燃。大致也就是顯示設(shè)置的新值、變化之前的舊值或者是一對多屬性的添加力惯、移除碗誉、替換的對象和序號(index)等。具體的信息可以command
+observeValueForKeyPath
父晶,查看通知處理方法的官方注釋哮缺,寫的非常詳細(xì)。
移除
待屬性值發(fā)生變化的通知處理完畢之后甲喝,我們需要對注冊的觀察者進行手動解除尝苇,解除的方法是:
- (void)removeObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context
對沒有沒有進行監(jiān)聽的屬性(keyPath)執(zhí)行解除操作,會拋出異常埠胖。同樣糠溜,如果對已經(jīng)注冊的監(jiān)聽屬性沒有執(zhí)行解除操作,也會拋出異常直撤。
自動通知和手動通知
如果按照以上操作步驟執(zhí)行非竿,則默認(rèn)使用的是自動通知,即只要對屬性值進行重新賦值(不管新值和舊值是否相同)谋竖,觀察者都會收到通知红柱。而在實際應(yīng)用中,有可能我們想根據(jù)自己的需要蓖乘,待屬性值滿足我們的條件之后才給觀察者發(fā)送通知锤悄,這時候我們就需要通過手動模式修改發(fā)送通知的條件和時間來達到目的了。
自動通知1
如上述操作驱敲,發(fā)送通知的時機和條件無法進行修改铁蹈。
//第一次修改可以正常接收到通知
[person setValue:@"hexintao" forKey:@"name"];
//自動通知模式下宽闲,接下來這兩次依然會接收到通知
[person setValue:@"hexintao" forKey:@"name"];
[person setValue:@"hexintao" forKey:@"name"];
當(dāng)然众眨,實際應(yīng)用中握牧,連續(xù)賦同樣的值的情況不多見也不推薦,我們只是為了以此說明自動通知模式下的情況娩梨。
手動通知
首先需要關(guān)閉自動通知沿腰,在被觀察者的類中重寫類方法:
(2017.01.04修改:如果不重寫這個類方法,則系統(tǒng)會在屬性 setter 方法的之前之后自動調(diào)用 willChangeValueForKey
和 didChangeValueForKey
狈定,造成同一次屬性的修改調(diào)用兩次 KVO 監(jiān)聽方法颂龙。)
+ (BOOL)automaticallyNotifiesObserversOf<Key>
{
return NO;
}
或者是:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = YES;
if ([key isEqualToString:@"name"])
{
automatic = NO;
}
else
{
automatic = [super automaticallyNotifiesObserversForKey:key];
}
}
后一個方法較復(fù)雜,而且有把屬性名稱(key
)拼錯的風(fēng)險纽什,所以還是推薦使用第一種方法措嵌。
接下來,要在需要發(fā)出通知的地方手動調(diào)用兩個方法芦缰,這個例子中我們就取屬性值的setter
方法:
- (void)setName:(NSString *)name
{
if (name != _name)
{
//子類不能重寫這兩個方法企巢,否則無法完成手動觸發(fā)KVO
//通過改變這兩個方法的位置,可以自定義KVO觸發(fā)的條件
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
上述兩個方法一定要成對調(diào)用才會成功發(fā)出通知让蕾。
剛才那個例子:
//第一次修改可以正常接收到通知
[person setValue:@"hexintao" forKey:@"name"];
//手動通知模式下浪规,接下來這兩次不會接收到通知
[person setValue:@"hexintao" forKey:@"name"];
[person setValue:@"hexintao" forKey:@"name"];
依賴鍵的注冊
有時候,我們監(jiān)聽的某個屬性值可能會依賴于其他多個屬性探孝,只要其他屬性發(fā)生了改變都會導(dǎo)致我們監(jiān)聽的屬性發(fā)生變化笋婿,這種就叫做依賴鍵。例如顿颅,在Person
類中有一個personInfo
的屬性缸濒,它返回的是對象的name
和age
的組合:
- (NSString *)personInfo
{
return [NSString stringWithFormat:@"person's name:%@ age:%d", self.name, self.age];
}
如果我們對personInfo
進行監(jiān)聽,則name
和age
的變化也會導(dǎo)致personInfo
發(fā)生變化粱腻,這時候我們就需要設(shè)置依賴鍵绍填。
+ (NSSet *)keyPathsForValuesAffectingPersonInfo
{
return [NSSet setWithObjects:@"name", @"age", nil];
}
然后再對personInfo
進行注冊監(jiān)聽,之后如果我們對name
或者age
的值進行修改的時候栖疑,觀察者就會收到這樣的通知:
CollectionViewTest[742:17933] {
kind = 1;
new = "person's name:hexintao age:0";
}
也就是當(dāng)關(guān)聯(lián)屬性中的任何一個發(fā)生了變化讨永,我們監(jiān)聽的這個屬性就會收到通知,說明其值發(fā)生了變化遇革。
集合屬性的監(jiān)聽
集合屬性整體還是部分卿闹?
一對一屬性的監(jiān)聽相對來說比較簡單,只要值發(fā)生了變化我們收到通知進行處理即可萝快。對于一對多集合類的屬性來說锻霎,牽扯到是監(jiān)聽整個集合發(fā)生的變化還是其中元素的變化?這兩種行為都可以通過KVO監(jiān)聽到揪漩,不過日常使用來說旋恼,我們更傾向于監(jiān)聽后者。
對于集合屬性奄容,正常的添加或刪除對象的操作并不能觸發(fā)KVO冰更,例如[person.personFriends addObject:@"2in"]
产徊,觀察者并不會收到變化通知,不過對于集合屬性整體的改變蜀细,例如person.personFriends = [[NSMutableArray alloc]init]
舟铜,觀察者可以正常收到通知。不過我們重點討論通過特定的方法監(jiān)聽集合屬性中對象的變化奠衔。大致分為兩種方法:
手動監(jiān)聽
集合屬性的手動監(jiān)聽即:在被監(jiān)聽的類中重寫一些修改集合元素的方法谆刨,之后調(diào)用這些方法對屬性進行修改就可以觸發(fā)KVO監(jiān)聽。
我們可以根據(jù)需要實現(xiàn):
//插入對象
- (void)insertObject:(id)object in<Key>AtIndex:(NSUInteger)index
//移除對象
-(void)removeObjectFrom<Key>AtIndex:(NSUInteger)index
具體的操作元素的方法可見官方手冊:KVC官方指導(dǎo)
之后我們通過調(diào)用重寫后的方法修改集合屬性時即可觸發(fā)KVO归斤。
自動監(jiān)聽
自動監(jiān)聽大致是:通過mutableArrayValueForKey
方法獲得一個可變對象的代理痊夭,對其進行修改即可自動觸發(fā)KVO。而valueForKey
返回的則是不可變對象脏里。
使用樣例:
[[person mutableArrayValueForKey:@"personFriends"] addObject:@"3in"];
// 但是如果是將上述操作賦值給一個可變數(shù)組生兆,再調(diào)用正常的類似于addObject方法將不會觸發(fā)KVO監(jiān)聽。
NSMutableArray *friends = [person mutableArrayValueForKey:@"personFriends"];
//不能觸發(fā)KVO模式
[person.personFriends addObject:@"4in"];
//這兩個方法能觸發(fā)KVO膝宁,但是friends和person.personFriends指向的并不是同一個對象鸦难,不過其內(nèi)容卻完全一樣,
//對friends操作會影響到person.personFriends的值员淫,反過來也是如此合蔽!
[friends insertObject:@"5in" atIndex:0]; //可以觸發(fā)KVO
[friends removeObjectAtIndex:0]; //可以觸發(fā)KVO