KVO底層分析

KVO概念

KVO ->Key-Value observing吝羞,鍵值觀察,當(dāng)被觀察對象中指定屬性發(fā)現(xiàn)變化時仔掸,觀察者就可以得到通知脆贵,進而進行后續(xù)操作。

KVO使用

根據(jù)KVO官方文檔 得知起暮,正常使用大體分為以下流程:

  • 注冊觀察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
  • KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@",change[NSKeyValueChangeNewKey]);
    }
}
  • 移除觀察者
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];

context 用法及意義

截屏2022-01-04 下午5.54.09.png

根據(jù)官方文檔解讀可以得知:
context上下文主要作用就是防止根據(jù)keypath查詢通知來源時卖氨,因父類子類觀察到相同路徑而出現(xiàn)的問題负懦,不同的keypath創(chuàng)建不同的context筒捺,這樣就可以不用通過字符串比較的方式去確定keypath,從而提高性能以及代碼可讀性纸厉。

//定義context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;

//注冊觀察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    
    
//KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (context == PersonNickContext) {
        NSLog(@"%@",change);
    }else if (context == PersonNameContext){
        NSLog(@"%@",change);
    }
}

觀察者移除

截屏2022-01-04 下午6.04.25.png

根據(jù)官方文檔可以得知:

  • 如果沒有注冊而移除了觀察者系吭,時就會出現(xiàn)NSRangeException,如果進行了一次addObserver颗品,則對應(yīng)的也需要removeObserver

  • 釋放后肯尺,觀察者不會自動將其自身移除。被觀察對象繼續(xù)發(fā)送通知躯枢,而忽略了觀察者的狀態(tài)则吟。但是,與發(fā)送到已釋放對象的任何其他消息一樣锄蹂,更改通知會觸發(fā)內(nèi)存訪問異常氓仲。因此,您可以確保觀察者在從內(nèi)存中消失之前將自己刪除得糜。

  • 該協(xié)議無法詢問對象是觀察者還是被觀察者敬扛。構(gòu)造代碼以避免發(fā)布相關(guān)的錯誤。一種典型的模式是在觀察者初始化期間(例如朝抖,在init或viewDidLoad中)注冊為觀察者啥箭,并在釋放過程中(通常在dealloc中)注銷,以確保成對和有序地添加和刪除消息治宣,并確保觀察者在注冊之前被取消注冊捉蚤,從內(nèi)存中釋放出來

總結(jié):KVO注冊觀察者(addObserver)與移除觀察者(removeObserver)是成對出現(xiàn)的抬驴,如果只注冊炼七,不移除缆巧,則會出現(xiàn)野指針類型的崩潰,如果只移除豌拙,不注冊陕悬,則會NSRangeException

KVO 的自動與手動觸發(fā)

系統(tǒng)默認的事自動觸發(fā),即如果添加了觀察者按傅,并且回調(diào)方法中存在相應(yīng)的處理捉超,這時只要屬性值發(fā)生改變,就會調(diào)用唯绍,如果關(guān)閉自動觸發(fā)automaticallyNotifiesObserversForKey設(shè)置為NO拼岳,這時就需要將需要觀察的屬性改變前增加willChangeValueForKey,改變后增加didChangeValueForKey况芒,這樣就可以觸發(fā)惜纸,通過手動觸發(fā)能夠更好地貼合項目中的需求,增加擴展性

KVO一對多

通過keyPathsForValuesAffectingValueForKey方法將多個屬性合并成一個進行觀察绝骚,以下載進度為例

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
self.person.writtenData += 10;
self.person.totalData  += 1;
[self.person removeObserver:self forKeyPath:@"downloadProgress"];
- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

KVO 鍵值變化類型

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//設(shè)值
    NSKeyValueChangeInsertion = 2,//插入
    NSKeyValueChangeRemoval = 3,//移除
    NSKeyValueChangeReplacement = 4,//替換
};
//根據(jù)下面代碼來實現(xiàn)不同的kind 
self.student.name = [NSString stringWithFormat:@"%@+",self.student.name];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
[[self.person mutableArrayValueForKey:@"dateArray"] removeLastObject];
[[self.person mutableArrayValueForKey:@"dateArray"] replaceObjectAtIndex:0 withObject:@"3"];
  • 注:當(dāng)當(dāng)前觀察對象為數(shù)組時耐版,不可以直接通過賦值的方式進行更改,而是需要mutableArrayValueForKeymutableArrayValueForKeyPath進行獲取压汪,然后才能實現(xiàn)更改回調(diào)

KVO 底層探究

由于KVO沒有對應(yīng)的開源代碼粪牲,故而通過跟流程的方式查看

  • 觀察的是setter方法
    驗證:根據(jù)屬性成員變量的區(qū)別可以得知,兩者之間屬性存在setter方法止剖,而成員變量沒有
@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

@end
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"實際情況:%@-%@",self.person.nickName,self.person->name);
    self.person.nickName = @"KC";
     self.person->name    = @"Cooci";
}

#pragma mark - KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

截屏2022-01-05 上午10.21.23.png

根據(jù)上述結(jié)果可以得知腺阳,屬性nickName更改成功收到回調(diào),而成員變量name沒有收到回調(diào)穿香,故而可以驗證KVO觀察的是setter方法

  • 中間類
    根據(jù)官方文檔得知亭引,在注冊觀察者之后,觀察對象的isa會發(fā)生改變
    截屏2022-01-05 上午10.32.13.png

    根據(jù)斷點獲取className可以看出isa的指向確實發(fā)生了改變
 [self printClasses:[LGPerson class]];
    self.person = [[LGPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
//     [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self printClasses:[LGPerson class]];
    [self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
    [self printClassAllMethod:[LGStudent class]];

#pragma mark - 遍歷方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[I];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
    
    // 注冊類的總數(shù)
    int count = objc_getClassList(NULL, 0);
    // 創(chuàng)建一個數(shù)組扔水, 其中包含給定對象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 獲取所有已注冊的類
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[I]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

通過遍歷方法和子類可以看出


截屏2022-01-05 上午10.30.19.png
  • 在沒有注冊觀察值之前痛侍,只存在LGPersonLGStudent兩個類,而注冊完成后魔市,多出了一個NSKVONotifying_LGPerson主届,故而可以確定,NSKVONotifying_LGPersonLGPerson的子類
  • 通過觀察NSKVONotifying_LGPerson中的方法列表可以看出待德,子類中存在setter方法君丁,在LGStudent中實現(xiàn)了setNickName的重寫,與NSKVONotifying_LGPerson中一致将宪,故而可以確定绘闷,在NSKVONotifying_LGPerson實現(xiàn)了setter橡庞、class、dealloc印蔗、_isKVO重寫扒最,而非繼承
截屏2022-01-05 下午1.23.08.png
  • 根據(jù)上圖可以得知,在移除觀察者之前isa指向的是子類NSKVONotifying_LGPerson华嘹,移除之后指回LGPerson
  • 雖然觀察者移除了吧趣,但是在其它頁面查看LGPerson子類時可以發(fā)現(xiàn)中間類NSKVONotifying_LGPerson沒有銷毀,這樣可以避免每次進行創(chuàng)建而造成性能低下耙厚,通過重用的方式使得中間類一經(jīng)創(chuàng)建就一直存在

KVO自定義實現(xiàn)

完整代碼只是基本實現(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末强挫,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子薛躬,更是在濱河造成了極大的恐慌俯渤,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件型宝,死亡現(xiàn)場離奇詭異八匠,居然都是意外死亡,警方通過查閱死者的電腦和手機诡曙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門臀叙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人价卤,你說我怎么就攤上這事劝萤。” “怎么了慎璧?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵床嫌,是天一觀的道長。 經(jīng)常有香客問我胸私,道長厌处,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任岁疼,我火速辦了婚禮阔涉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘捷绒。我一直安慰自己瑰排,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布暖侨。 她就那樣靜靜地躺著椭住,像睡著了一般。 火紅的嫁衣襯著肌膚如雪字逗。 梳的紋絲不亂的頭發(fā)上京郑,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天宅广,我揣著相機與錄音,去河邊找鬼些举。 笑死跟狱,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的金拒。 我是一名探鬼主播兽肤,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绪抛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起电禀,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤幢码,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后尖飞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體症副,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年政基,在試婚紗的時候發(fā)現(xiàn)自己被綠了贞铣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡沮明,死狀恐怖辕坝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情荐健,我是刑警寧澤酱畅,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站江场,受9級特大地震影響纺酸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜址否,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一餐蔬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧佑附,春花似錦樊诺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瘟斜,卻和暖如春缸夹,著一層夾襖步出監(jiān)牢的瞬間痪寻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工虽惭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留橡类,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓芽唇,卻偏偏與公主長得像顾画,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子匆笤,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

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