iOS KVO原理探究

KVO原理探究.jpeg

導(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:

  1. KVO基本使用
  2. KVO觸發(fā)模式
  3. KVO屬性依賴
  4. KVO基本原理
  5. KVO容器觀察

1. KVO基本使用

1.1 使用KVO分為三個(gè)步驟:

  1. 通過(guò)addObserver:forKeyPath:options:context:方法注冊(cè)觀察者幔嫂,觀察者可以接收keyPath屬性的變化事件。
  2. 在觀察者中實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:方法誊薄,當(dāng)keyPath屬性發(fā)生改變后履恩,KVO會(huì)回調(diào)這個(gè)方法來(lái)通知觀察者。
  3. 當(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)姨谷,需要以下步驟:

  1. 創(chuàng)建一個(gè)子類,例如KVOModel映九,子類名字就會(huì)叫做NSKVONotifying_KVOModel梦湘。
  2. 重寫(xiě)set方法,例如觀察name件甥,底層就會(huì)重寫(xiě)setName方法捌议。
  3. 外界改變isa指針。

可以通過(guò)查看KVOModel驗(yàn)證下系統(tǒng)的實(shí)現(xiàn)引有,在KVOModel創(chuàng)建后查看下對(duì)應(yīng)的信息瓣颅,如下:

KVOModel對(duì)象信息.png
調(diào)用完addObserver,再次查看KVOModel對(duì)象的信息譬正,如下:
KVOModel對(duì)象信息.png

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)如下:
tmpArray信息.png

通過(guò)mutableArrayValueForKey方法獲取賦值楚昭,再次查看tmpArray的結(jié)構(gòu)信息,如下:
tmpArray信息.png
tmpArray類型改變了拍顷,很明顯是個(gè)子類抚太,所以應(yīng)該是系統(tǒng)在子類中重寫(xiě)了add方法,然后調(diào)用willChangeValueForKey:和didChangeValueForKey:兩個(gè)方法通知外部達(dá)到目的昔案。

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觀察哮幢,很方便使用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末志珍,一起剝皮案震驚了整個(gè)濱河市橙垢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伦糯,老刑警劉巖柜某,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗽元,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡喂击,警方通過(guò)查閱死者的電腦和手機(jī)剂癌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)翰绊,“玉大人佩谷,你說(shuō)我怎么就攤上這事〖嗍龋” “怎么了谐檀?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)秤茅。 經(jīng)常有香客問(wèn)我稚补,道長(zhǎng)童叠,這世上最難降的妖魔是什么框喳? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮厦坛,結(jié)果婚禮上五垮,老公的妹妹穿的比我還像新娘。我一直安慰自己杜秸,他們只是感情好放仗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著撬碟,像睡著了一般诞挨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上呢蛤,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天惶傻,我揣著相機(jī)與錄音,去河邊找鬼其障。 笑死银室,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的励翼。 我是一名探鬼主播蜈敢,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼汽抚!你這毒婦竟也來(lái)了抓狭?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤造烁,失蹤者是張志新(化名)和其女友劉穎否过,沒(méi)想到半個(gè)月后狱从,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叠纹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年季研,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片誉察。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡与涡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出持偏,到底是詐尸還是另有隱情驼卖,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布鸿秆,位于F島的核電站酌畜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏卿叽。R本人自食惡果不足惜桥胞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望考婴。 院中可真熱鬧贩虾,春花似錦、人聲如沸沥阱。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)考杉。三九已至策精,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間崇棠,已是汗流浹背咽袜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留易茬,地道東北人酬蹋。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像抽莱,于是被迫代替她去往敵國(guó)和親范抓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,392評(píng)論 8 265
  • 本篇會(huì)對(duì)KVO的實(shí)現(xiàn)進(jìn)行探究食铐,不涉及太多KVO的使用方法匕垫,但是會(huì)有一些使用時(shí)的思考。 一虐呻、使用上的疑問(wèn) 1.key...
    奮拓達(dá)閱讀 507評(píng)論 0 2
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,103評(píng)論 1 32
  • KVC KVC定義 KVC(Key-value coding)鍵值編碼象泵,就是指iOS的開(kāi)發(fā)中寞秃,可以允許開(kāi)發(fā)者通過(guò)K...
    暮年古稀ZC閱讀 2,145評(píng)論 2 9
  • 不要再給他找借口,就是他不愛(ài)我E蓟荨4菏佟!
    魚(yú)骨頭_閱讀 204評(píng)論 0 0