導(dǎo)語(yǔ):
KVO全稱Key Value Observing疲憋,直譯為鍵值觀察。KVO 作為 iOS 中一種強(qiáng)大并且有效的機(jī)制庸诱,為 iOS 開(kāi)發(fā)者們提供了很多的便利叨粘;可以使用 KVO 來(lái)檢測(cè)對(duì)象屬性的變化、快速做出響應(yīng)旗芬,這能夠?yàn)殚_(kāi)發(fā)者在開(kāi)發(fā)強(qiáng)交互舌胶、響應(yīng)式應(yīng)用以及實(shí)現(xiàn)視圖和模型的雙向綁定時(shí)提供大量的幫助。
Demo源碼見(jiàn)KVODemo疮丛,主要從以下5個(gè)方面來(lái)探究KVO:
- KVO基本使用
- KVO觸發(fā)模式
- KVO屬性依賴
- KVO基本原理
- KVO容器觀察
1. KVO基本使用
1.1 使用KVO分為三個(gè)步驟:
- 通過(guò)
addObserver:forKeyPath:options:context:
方法注冊(cè)觀察者幔嫂,觀察者可以接收keyPath屬性的變化事件。 - 在觀察者中實(shí)現(xiàn)
observeValueForKeyPath:ofObject:change:context:
方法誊薄,當(dāng)keyPath屬性發(fā)生改變后履恩,KVO會(huì)回調(diào)這個(gè)方法來(lái)通知觀察者。 - 當(dāng)觀察者不需要監(jiān)聽(tīng)時(shí)暇屋,可以調(diào)用
removeObserver:forKeyPath:
方法將KVO移除似袁。 需注意調(diào)用removeObserver需要在觀察者消失之前洞辣,否則會(huì)導(dǎo)致Crash咐刨。
1.2 addObserver方法
在注冊(cè)觀察者時(shí)昙衅,可以傳入下列參數(shù):
Observer參數(shù),觀察者對(duì)象定鸟。
keyPath參數(shù)而涉,需要觀察的屬性。由于是字符串形式联予,傳錯(cuò)容易導(dǎo)致Crash啼县。一般利用系統(tǒng)的反射機(jī)制NSStringFromSelector(@selector(keyPath))。
-
options參數(shù)沸久,參數(shù)是一個(gè)枚舉類型季眷。
NSKeyValueObservingOptionNew
接收新值,默認(rèn)為只接收新值
NSKeyValueObservingOptionOld
接收舊值
NSKeyValueObservingOptionInitial
在注冊(cè)時(shí)立即接收一次回調(diào),在改變時(shí)也會(huì)發(fā)送通知
NSKeyValueObservingOptionPrior
改變之 前發(fā)一一次卷胯,改變之后發(fā)-一次 Context參數(shù)子刮,傳入任意類型的對(duì)象,在接收消息回調(diào)的代碼中可以接收到這個(gè)對(duì)象窑睁,是KVO中的一種傳值方式挺峡。
注意:調(diào)用addObserver方法后,KVO并不會(huì)對(duì)觀察者進(jìn)行強(qiáng)引用担钮,所以需要注意觀察者的生命周期橱赠,否則會(huì)導(dǎo)致觀察者被釋放帶來(lái)的Crash。
1.3 監(jiān)聽(tīng)回調(diào)
觀察者需要實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:方法箫津,如果沒(méi)有實(shí)現(xiàn)會(huì)導(dǎo)致Crash狭姨。
里面參數(shù):
-
keyPath
: 監(jiān)聽(tīng)屬性名稱 -
Object
: 被觀察對(duì)象 -
Change
: 字典,字典中存放KVO屬性相關(guān)的值苏遥,根據(jù)options時(shí) 傳入的枚舉來(lái)返回送挑。 -
Context
: 傳入進(jìn)來(lái)的上下文,一般在添加觀察者時(shí)暖眼,留下一個(gè)入口惕耕,用于傳值。
Demo如下:
NS_ASSUME_NONNULL_BEGIN
@interface KVOModel : NSObject
@property (nonatomic, strong) NSString* name;
@end
NS_ASSUME_NONNULL_END
有個(gè)class為KVOModel诫肠,需要對(duì)類中的name屬性進(jìn)行監(jiān)聽(tīng)
@interface ViewController ()
@property (nonatomic, strong) KVOModel* model;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_model = [KVOModel new];
// 注冊(cè)
[_model addObserver:self forKeyPath:NSStringFromSelector(@selector(name)) options:(NSKeyValueObservingOptionNew) context:nil];
}
/** 監(jiān)聽(tīng)方法 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"%@", change);
}
/** 屏幕touch */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int magicNum;
_model.name = [NSString stringWithFormat:@"name=%d", magicNum++];
}
運(yùn)行結(jié)果:
2018-11-23 00:47:57.035430+0800 KVODemo[40822:260341] {
kind = 1;
new = "name=0";
}
2018-11-23 00:47:58.197815+0800 KVODemo[40822:260341] {
kind = 1;
new = "name=1";
}
2. KVO觸發(fā)模式
KVO在屬性發(fā)生改變時(shí)的調(diào)用是自動(dòng)的司澎,如果想要手動(dòng)控制這個(gè)調(diào)用時(shí)機(jī),或想自已實(shí)現(xiàn)KVO屬性的調(diào)用栋豫,則可以通過(guò)KVO提供的方法進(jìn)行調(diào)用挤安。
@implementation KVOModel
/** 模式調(diào)整 */
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
return NO; // 改為手動(dòng)模式
}
@end
這樣在ViewController中改name的值不會(huì)進(jìn)到監(jiān)聽(tīng)方法中,需要手動(dòng)調(diào)用觸發(fā)丧鸯,在更改name地方需要做如下處理:
/** 屏幕touch */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int magicNum;
[_model willChangeValueForKey:NSStringFromSelector(@selector(name))];
_model.name = [NSString stringWithFormat:@"name=%d", magicNum++];
[_model didChangeValueForKey:NSStringFromSelector(@selector(name))];
}
手動(dòng)模式的好處蛤铜,可能有一種需求在某些情況下在更改value的時(shí)候,不需要通知,有的時(shí)候需要通知围肥,這個(gè)時(shí)候就需要手動(dòng)模式來(lái)處理剿干。
如果把上面代碼中對(duì)model中的name賦值給注視掉,再次去點(diǎn)擊屏幕穆刻,會(huì)發(fā)現(xiàn)還是會(huì)進(jìn)到監(jiān)聽(tīng)方法中置尔,這種情況下,監(jiān)聽(tīng)方法調(diào)不調(diào)用與設(shè)置name無(wú)關(guān)氢伟,只是和有沒(méi)有調(diào)用方法willChangeValueForKey:和didChangeValueForKey:有關(guān)榜轿。
3. KVO屬性依賴
在開(kāi)發(fā)過(guò)程中,model一般不會(huì)那么簡(jiǎn)單朵锣,比如KVOModel中有個(gè)Person類的屬性谬盐,要觀察Person屬性中的屬性變化,就不能上面那樣方法進(jìn)行處理诚些,Person類:
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
KVOModel改為如下:
@interface KVOModel : NSObject
@property (nonatomic, strong) NSString* name;
@property (nonatomic, strong) Person* person;
@end
在ViewController注冊(cè)的時(shí)候要做如下處理:
// 注冊(cè)
[_model addObserver:self forKeyPath:@"person.age" options:(NSKeyValueObservingOptionNew) context:nil];
在Person中只有一個(gè)age屬性设褐,如果還有其他屬性,可以在上面注冊(cè)代碼下加入一樣的代碼泣刹,只是更改person.age值助析。但是如果是Person中的屬性很多很多,每個(gè)屬性更改都要通知觀察者椅您,這樣寫(xiě)就比較麻煩外冀,這個(gè)時(shí)候就要通過(guò)屬性依賴進(jìn)行處理。需要重寫(xiě)KVOModel中類方法:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet* keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"person"])
{
keyPaths = [[NSSet alloc] initWithObjects:@"_person.age", @"_person.name", nil];
}
return keyPaths;
}
在ViewController注冊(cè)掀泳,只要監(jiān)聽(tīng)person屬性就可以
[_model addObserver:self forKeyPath:@"person" options:(NSKeyValueObservingOptionNew) context:nil];
這樣更改person對(duì)象的屬性值雪隧,都會(huì)通知到觀察者KVOModel,進(jìn)入到監(jiān)聽(tīng)方法员舵。
4. KVO基本原理
KVO是通過(guò)觀察屬性的set方法脑沿,但是前面demo中不設(shè)置屬性值,只要調(diào)用willChangeValueForKey:和didChangeValueForKey:兩個(gè)方法也會(huì)觸發(fā)通知马僻,這個(gè)可以通過(guò)demo驗(yàn)證庄拇,比如說(shuō)KVOModel中有個(gè)成員變量value,直接更改value的值看效果韭邓。
@interface KVOModel : NSObject
{
@public
int value;
}
@end
注冊(cè)時(shí)keyPath為value措近,然后去更改值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int magicNum;
_model->value = magicNum++;
}
會(huì)發(fā)現(xiàn)監(jiān)聽(tīng)方法沒(méi)有調(diào)用到,KVO實(shí)際上還是通過(guò)觀察屬性set的方法達(dá)到目的女淑。 那如何當(dāng)調(diào)用KVOModel類對(duì)象的set方法能夠觀察到瞭郑,首先會(huì)想到兩種方法,一種是分類去設(shè)計(jì)鸭你,另一種是通過(guò)繼承屈张,子類實(shí)現(xiàn)擒权。
分類實(shí)現(xiàn)
可以直接在分類中重寫(xiě)屬性對(duì)應(yīng)的set方法,然后在分類set方法去通知外部阁谆,但是這種會(huì)有弊端碳抄,有的時(shí)候會(huì)有種需求,會(huì)重寫(xiě)set方法笛厦,然后加上自己的業(yè)務(wù)纳鼎,如果KVO通過(guò)分類實(shí)現(xiàn)俺夕,就會(huì)覆蓋掉原來(lái)的set方法裳凸,業(yè)務(wù)邏輯就永遠(yuǎn)調(diào)用不到,這種框架設(shè)計(jì)就會(huì)有問(wèn)題劝贸。
子類實(shí)現(xiàn)
KVO底層實(shí)現(xiàn)通過(guò)子類實(shí)現(xiàn)姨谷,需要以下步驟:
- 創(chuàng)建一個(gè)子類,例如KVOModel映九,子類名字就會(huì)叫做NSKVONotifying_KVOModel梦湘。
- 重寫(xiě)set方法,例如觀察name件甥,底層就會(huì)重寫(xiě)setName方法捌议。
- 外界改變isa指針。
可以通過(guò)查看KVOModel驗(yàn)證下系統(tǒng)的實(shí)現(xiàn)引有,在KVOModel創(chuàng)建后查看下對(duì)應(yīng)的信息瓣颅,如下:
isa就會(huì)被改為NSKVONotifying_KVOModel宫补,這個(gè)肯定是在調(diào)用addObserver方法中創(chuàng)建了這個(gè)子類,蘋(píng)果的KVO沒(méi)有開(kāi)源曾我,網(wǎng)絡(luò)上有基于
GNU開(kāi)源
的代碼粉怕,會(huì)有共通之處,可以查看參考抒巢。我的另一篇簡(jiǎn)書(shū)文章《自定義KVO》大致根據(jù)原理模擬實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的KVO贫贝。
5. KVO容器觀察
如果KVOModel中有個(gè)容器屬性,這需要怎么觀察到容器中數(shù)據(jù)改動(dòng)蛉谜。
@property (nonatomic, strong) NSMutableArray* arrayValue;
同樣通過(guò)上述注冊(cè)方法對(duì)arrayValue進(jìn)行觀察平酿,然后每次點(diǎn)擊屏幕的時(shí)候都給arrayValue添加一個(gè)元素,如下:
[_model.arrayValue addObject:@"1"];
會(huì)發(fā)現(xiàn)回調(diào)方法不會(huì)觸發(fā)悦陋,這個(gè)由于KVO觀察的是set方法蜈彼,這邊容器是add,所以就不會(huì)觸發(fā)俺驶,KVO給開(kāi)發(fā)者提供了mutableArrayValueForKey去拿容器對(duì)象幸逆,然后再調(diào)用add棍辕,這個(gè)時(shí)候就會(huì)觀察到元素改變:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSMutableArray* tmpArray = [_model mutableArrayValueForKey:@"arrayValue"];
[tmpArray addObject:@"1"];
}
通過(guò)打斷點(diǎn),查看tmpArray信息还绘,剛開(kāi)始定義的時(shí)候結(jié)構(gòu)如下:6. 結(jié)尾
蘋(píng)果的KVO技術(shù)給我們提供了方便尿贫,但是用不適當(dāng),可能就會(huì)出現(xiàn)crash踏揣,觀察Demo中庆亡,在初始化地方注冊(cè),然后用其他方法來(lái)監(jiān)聽(tīng)捞稿,明顯是分開(kāi)的又谋,如果代碼量很大的時(shí)候,這種方式就比較不好娱局,可讀性就比較差點(diǎn)彰亥,并且在dealloc的時(shí)候,一定要remove監(jiān)聽(tīng)衰齐,必須要一一對(duì)應(yīng)任斋,如果注冊(cè)多了或者remove多了,都會(huì)crash娇斩,針對(duì)KVO這點(diǎn)缺點(diǎn)仁卷,可以對(duì)其進(jìn)行封裝,比如RAC(函數(shù)式響應(yīng)式編程)框架犬第,在github中有個(gè)開(kāi)源框架ReactiveCocoa锦积,函數(shù)式就是AFN,KVO封裝可以結(jié)合Block去做歉嗓,addObserver:forKeyPath:options:context:
調(diào)用的時(shí)候就沒(méi)有必要傳self丰介,因?yàn)橥ㄟ^(guò)block的時(shí)候,就不用根據(jù)self去調(diào)用監(jiān)聽(tīng)方法observeValueForKeyPath:ofObject:change:context:
邏輯直接就是調(diào)用block鉴分,這樣也就不用在dealloc的時(shí)候去remove觀察哮幢,很方便使用。