20-KVO分析

前言

什么是KVO(Key-Value Observing)

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
鍵值觀察是一種機(jī)制壳咕,它允許對(duì)象在其他對(duì)象的指定屬性發(fā)生更改時(shí)收到通知微王。

KVO官方地址

KVO基礎(chǔ)

KVO 從日常的開發(fā)中看出無非就是三個(gè)api

  • (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
  • (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
  • (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

那么接下來就具體看看這幾個(gè)API到底有何作用。

1尘喝、NSKeyValueObservingOptions 的作用。

NSKeyValueObservingOptionOldNSKeyValueObservingOptionNew 是我們常用的兩個(gè)選選項(xiàng)斋陪。
下面通過一個(gè) demo 來驗(yàn)證這個(gè)到底有什么作用
先準(zhǔn)備如下一份代碼

@interface CDPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, strong) NSMutableArray *dateArray;
@property (nonatomic, copy) NSArray *array;
@end

///實(shí)現(xiàn)如下一份代碼
- (void)viewDidLoad {
    [super viewDidLoad]; 
    self.person = [CDPerson alloc];
    self.person.nick = @"Hello"; 
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionOld) context:NULL];
  ///  [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
  ///  [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionPrior) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"change = %@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.nick = [self.person.nick stringByAppendingString:@"+"];
}

這時(shí)候我們分別監(jiān)聽?zhēng)讉€(gè)不同的 options 朽褪,可以得到如下的結(jié)果

  1. NSKeyValueObservingOptionOld
change = {
    kind = 1;
    old = Hello;
}
  1. NSKeyValueObservingOptionNew
change = {
    kind = 1;
    new = "Hello+";
}
  1. NSKeyValueObservingOptionPrior
change = {
    kind = 1;
    notificationIsPrior = 1;
}
change = {
    kind = 1;
}

2、 context

上下文无虚。這種設(shè)計(jì)在很多場(chǎng)景都有實(shí)用缔赠,特別是在CFCG等框架的時(shí)候友题。而從官方文檔上來看就是 :

一種更安全嗤堰、更可擴(kuò)展的方法是使用上下文來確保您收到的通知是發(fā)送給您的觀察者而不是超類的。
那么我們來驗(yàn)證一下

static void * personName = @"personName";
/// 2咆爽、驗(yàn)證 context
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:personName];
    

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == personName) {
        NSLog(@"%@", context);
    }
    NSLog(@"change = %@", change);
}

打印結(jié)果如下:

2021-07-29 23:02:04.270060+0800 001---KVO初探[10373:973646] change = {
    kind = 1;
    new = "Hello+";
}

2021-07-29 23:02:04.270130+0800 001---KVO初探[10373:973646] personName
2021-07-29 23:02:04.270192+0800 001---KVO初探[10373:973646] change = {
    kind = 1;
    new = "niubi-";
}

通過結(jié)果我們發(fā)現(xiàn)梁棠,這個(gè)context 確實(shí)可以被帶到通知里面去。這樣我們就可以更加好判斷誰監(jiān)聽的誰斗埂。也可以保證在移除觀察者的時(shí)候不會(huì)出現(xiàn)問題(不會(huì)把父類相同的監(jiān)聽給移除了)符糊。

// 這樣,即使父類也有一個(gè)觀察了name 的觀察者呛凶,只要context 不一樣男娄,就不會(huì)隨意的移除掉。
[self.person removeObserver:self forKeyPath:@"name" context:personName]

3、要不要移除觀察者

通常來說模闲,我們注冊(cè)的觀察者一旦執(zhí)行了 dealloc 以后建瘫,那么被觀察的對(duì)象也就釋放了。所以移除與否都沒有關(guān)系尸折。但是有一些情況是啰脚,雖然我的觀察者釋放了,但是這個(gè)被觀察的對(duì)象依然還存在实夹,那這個(gè)時(shí)候在給這個(gè)觀察者發(fā)生通知那就會(huì)出問題了橄浓。比如我們上面的被觀察的對(duì)象是個(gè)單列,或者其他一些暫時(shí)沒辦法釋放的東西亮航,那么下次在給當(dāng)前對(duì)象發(fā)生通知就會(huì)觸發(fā)野指針而崩潰荸实。
所以,最好還是在我們觀察者 dealloc 的時(shí)候缴淋,執(zhí)行 remove准给。

4、手動(dòng)和自動(dòng)監(jiān)聽KVO

api 里面還有一個(gè) +automaticallyNotifiesObserversForKey:方法重抖,這個(gè)方法默認(rèn)返回 true露氮。也就是默認(rèn)開啟自動(dòng)發(fā)送通知,如果我們返回 false 那么久沒發(fā)自動(dòng)發(fā)送通知仇哆,需要手動(dòng)發(fā)送通知沦辙,即調(diào)用 willChangeValueForKey:and didChangeValueForKey: 者兩個(gè)方法來手動(dòng)發(fā)出通知。也可以通過 + (BOOL)automaticallyNotifiesObserversOfName 這個(gè)方法來指定某個(gè)屬性是和否可以自動(dòng)發(fā)出通知(這個(gè)要在automaticallyNotifiesObserversForKey:沒有重寫的情況下)讹剔。

// 自動(dòng)開關(guān)關(guān)閉
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return false;
}

當(dāng)我們重寫了如上的方法后油讯,整個(gè)類的KVO 就不會(huì)自動(dòng)觸發(fā)通知的發(fā)送。這個(gè)時(shí)候就需要手動(dòng)去觸發(fā):

- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

5延欠、監(jiān)聽集合類型

如果我們要監(jiān)聽集合類型的屬性(如:NSArray)陌兑,那么我們實(shí)現(xiàn)如下監(jiān)聽。

  [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"array" options:(NSKeyValueObservingOptionNew) context:NULL];
    

如果直接改變數(shù)組的成員是不會(huì)觸發(fā)的由捎,只有按照KVC 的方式去觸發(fā)才可以觸發(fā)通知的發(fā)送兔综。

/// 這樣是不會(huì)生效的
[self.person.dateArray addObject:@"222"];

/// 需要下面這樣
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"222"];
[[self.person mutableArrayValueForKey:@"array"] addObject:@"333"];
// 亦或者 
    [[self.person mutableArrayValueForKey:@"dateArray"] removeObject:@"2"];
    [[self.person mutableArrayValueForKey:@"array"] removeObject:@"3"];

當(dāng)然這樣執(zhí)行集合類型的觀察在配合 options 可以看看是什么效果,閣下可以自己去嘗試看看結(jié)果是如何的狞玛。筆者這里就不在細(xì)說软驰,還有包括KVC 的相關(guān)的一些對(duì)應(yīng)的情況,可以查閱筆者關(guān)于KVC 的表述

6心肪、監(jiān)聽keyPath 多級(jí)路徑

self.person.st = [LGStudent alloc];
    self.person.st.name = @"student";
[self.person addObserver:self forKeyPath:@"st.name" options:(NSKeyValueObservingOptionNew) context:NULL]

//執(zhí)行如下方法
self.person.st.name = [self.person.st.name stringByAppendingString:@"+"];

///打印結(jié)果如下:
change = {
    kind = 1;
    new = "student+";
}
change = {
    kind = 1;
    new = "student++";
}

KVO 實(shí)現(xiàn)

KVO 到底是如何實(shí)現(xiàn)的锭亏,接下來我們就去探索。這里借助LLDBapi 來一起驗(yàn)證硬鞍。

1慧瘤、探索isa

Automatic key-value observing is implemented using a technique called isa-swizzling.
從官方文檔來看戴已,自動(dòng)KVO是一種 isa-swizzling,那么我們就先來看看這個(gè)isa 到底是什么锅减,如下實(shí)現(xiàn)一段代碼糖儡,并且下一個(gè)斷點(diǎn),分別在添加觀察者和添加后打印結(jié)果

查看isa

從結(jié)果我們可以看出怔匣,在添加了觀察者后握联,isa指向了一個(gè) 名為 NSKVONotifying_LGPerson 的類。那么這個(gè)類和我們的 LGPerson 有什么關(guān)系呢劫狠?那么結(jié)合我們前面類的原理里面探索的拴疤,類結(jié)構(gòu)的第二個(gè)成員變量是 superClass ,可以得出他們是父子關(guān)系独泞。

(lldb) po 0x00000001c28f8628
NSObject

(lldb) po 0x0000000104a55650
LGPerson

7、NSKVONotifying_CDPerson 里面有什么東西<成員變量苔埋、方法懦砂、協(xié)議>

這里筆者采用api來看看當(dāng)前這個(gè)類里面到底有什么。
接下來調(diào)用如下一個(gè)方法來探索這個(gè)類里面有什么成員组橄。

- (void)getAllMethodFromCls:(Class)cls {
    
    unsigned int count;
    Method *ms = class_copyMethodList(cls, &count);
    NSLog(@"**************** 方法: %@ : %d ****************", cls, count);
    for (int i = 0; i < count; i++) {
        SEL sel = method_getName(ms[I]);
        NSLog(@"SEL = %@", NSStringFromSelector(sel));
    }
     
    Ivar *ivs = class_copyIvarList(cls, &count);
    NSLog(@"**************** 成員變量: %@ : %d", cls, count);
    for (int i = 0; i < count; i++) {
        const char *cName = ivar_getName(ivs[I]);
        NSLog(@"Name = %@", [NSString stringWithCString:cName encoding:NSUTF8StringEncoding]);
    }
    
    objc_property_t *ps = class_copyPropertyList(cls, &count);
    NSLog(@"**************** 屬性: %@ : %d", cls, count);
    for (int i = 0; i < count; i++) {
        const char *cName = property_getName(ps[I]);
        NSLog(@"Name = %@", [NSString stringWithCString:cName encoding:NSUTF8StringEncoding]);
    }
    NSLog(@"\n\n");
     
}

然后在監(jiān)聽前后監(jiān)聽后分別查看這個(gè)類的相關(guān)信息

[self getAllMethodFromCls:object_getClass(self.person)];
   
    [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"st.name" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    [self getAllMethodFromCls:object_getClass(self.person)];
    

這里筆者有個(gè)問題是設(shè)個(gè) st.name 到底是在何處監(jiān)聽的荞膘?

觀察前的結(jié)果
觀察后的結(jié)果

從結(jié)果我們可以看到,并沒有setsSt.name 這樣的方法玉工。只有一個(gè) setSt:的方法羽资,這就讓我懷疑是不是 LGStudent 也有創(chuàng)建了一個(gè)動(dòng)態(tài)了的類,而這種多級(jí)監(jiān)聽最后只是通過kvc 傳遞到了里面相關(guān)的對(duì)象里面去了遵班。
通過調(diào)試我發(fā)現(xiàn)確實(shí)是這樣的屠升,LGStudent 耶動(dòng)態(tài)生成了一個(gè) NSKVONotifying_LGStudent 子類。

(lldb) po object_getClass(self.person.st)
NSKVONotifying_LGStudent

結(jié)論

經(jīng)過前面這么多分析狭郑,KVO 的大致流程和原理我們野梳理的差不多了腹暖。

1、動(dòng)態(tài)注冊(cè)子類 NSKVONotifying_XXX翰萨。
2脏答、判斷當(dāng)前是否是屬性(因?yàn)樾枰貙憇etter: 方法)。
3亩鬼、修改當(dāng)前對(duì)象isa指針指向動(dòng)態(tài)子類NSKVONotifying_XXX殖告。
4、調(diào)用setter 方法雳锋,并且轉(zhuǎn)發(fā)給父類同時(shí)發(fā)出通知通知觀察者observeValueForKeyPath: ofObject: change: context:黄绩。
5、在調(diào)用removeObserver:forKeyPath: 后有將isa 指回原來的類魄缚。

?著作權(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)離奇詭異诽里,居然都是意外死亡袒餐,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門谤狡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灸眼,“玉大人,你說我怎么就攤上這事墓懂⊙嫘” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵捕仔,是天一觀的道長匕积。 經(jīng)常有香客問我,道長榜跌,這世上最難降的妖魔是什么闪唆? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮钓葫,結(jié)果婚禮上悄蕾,老公的妹妹穿的比我還像新娘。我一直安慰自己础浮,他們只是感情好帆调,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著霸旗,像睡著了一般贷帮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诱告,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天撵枢,我揣著相機(jī)與錄音,去河邊找鬼精居。 笑死锄禽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的靴姿。 我是一名探鬼主播沃但,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼佛吓!你這毒婦竟也來了宵晚?” 一聲冷哼從身側(cè)響起垂攘,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎淤刃,沒想到半個(gè)月后晒他,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡逸贾,尸身上長有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
  • 文/蒙蒙 一崔慧、第九天 我趴在偏房一處隱蔽的房頂上張望拂蝎。 院中可真熱鬧,春花似錦惶室、人聲如沸温自。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悼泌。三九已至,卻和暖如春夹界,著一層夾襖步出監(jiān)牢的瞬間馆里,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國打工可柿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸠踪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓复斥,卻偏偏與公主長得像营密,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子目锭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

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