iOS-底層探索19:KVO原理

iOS 底層探索 文章匯總

目錄


一砾淌、什么是KVO

KVO是基于KVC的啦撮,全稱是Key-Value-Observer鍵值觀察者。KVO提供一種機(jī)制汪厨,指定一個(gè)被觀察的對(duì)象(A類)赃春,當(dāng)對(duì)象某個(gè)屬性(A中的屬性name)發(fā)生更改時(shí),對(duì)象會(huì)獲得通知劫乱,并作出相應(yīng)處理织中;【且不需要給被觀察的對(duì)象添加任何額外代碼,就能使用KVO機(jī)制】衷戈。KVOObjective-C對(duì)觀察者設(shè)計(jì)模式的一種實(shí)現(xiàn)狭吼。

KVOMVC設(shè)計(jì)架構(gòu)下的項(xiàng)目很適合實(shí)現(xiàn)Model模型和View視圖之間的通訊。
例如:代碼中殖妇,在模型類M創(chuàng)建屬性數(shù)據(jù)刁笙,在控制器中創(chuàng)建觀察者,一旦屬性數(shù)據(jù)發(fā)生改變觀察者就會(huì)收到通知,然后刷新相應(yīng)的視圖疲吸。

官方文檔:Key-Value Observing Programming Guide

二座每、KVO基本使用

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.person = [NAPerson new];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = @"differ";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    CJLog(@"%@",change[NSKeyValueChangeNewKey]);
}

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name" context:NULL];
}
1、context的作用

當(dāng)前觀察者觀察了多個(gè)對(duì)象摘悴,當(dāng)這些對(duì)象中有同名的KeyPath時(shí)可以用來(lái)區(qū)分是哪個(gè)context下的KeyPath尺栖。eg:

static void *PersonNameContext = &PersonNameContext;
static void *AnimalNameContext = &AnimalNameContext;

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[self.animal addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:AnimalNameContext];
2、是否有必要移除觀察者

有必要烦租。官方文檔提示如果沒(méi)移除觀察者在某些情況下會(huì)出現(xiàn)NSRangeException異常崩潰。

3除盏、手動(dòng)開(kāi)啟KVO

默認(rèn)情況下KVO都是自動(dòng)開(kāi)啟的叉橱,但是我們可實(shí)現(xiàn)手動(dòng)開(kāi)關(guān)KVO

@implementation NAPerson

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

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

@end
4、集合類型的觀察
[self.person addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew context:NULL];

//[self.person.dataArray addObject:@"1"];不起作用
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"1"];


三者蠕、KVO實(shí)現(xiàn)原理

@interface NAPerson : NSObject {
    @public
    NSString *_nickName;
}
@end

// ViewController
[self.person addObserver:self forKeyPath:@"_nickName" options:NSKeyValueObservingOptionNew context:NULL];

self.person->_nickName = @"differ";

[self.person removeObserver:self forKeyPath:@"_nickName" context:NULL];

通過(guò)對(duì)成員變量_nickName添加KVO觀察后可以發(fā)現(xiàn):KVO不會(huì)對(duì)成員變量進(jìn)行觀察窃祝,只對(duì)屬性進(jìn)行觀察。由此可猜想KVO和屬性的setter進(jìn)行了關(guān)聯(lián)踱侣。
通過(guò)官方文檔可知粪小,KVO會(huì)產(chǎn)生一個(gè)中間類,將觀察對(duì)象的isa指向了這個(gè)中間類抡句。斷點(diǎn)調(diào)試如下:

KVO產(chǎn)生的中間類是分類還是派生子類探膊?

修改代碼遍歷KVO觀察對(duì)象的類以及子類

- (void)viewDidLoad {
    [super viewDidLoad];

    [self printClasses:[NAPerson class]];
    self.person = [NAPerson new];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
    [self printClasses:[NAPerson class]];
}

#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls {
    // 注冊(cè)類的總數(shù)
    int count = objc_getClassList(NULL, 0);
    // 創(chuàng)建一個(gè)數(shù)組, 其中包含給定對(duì)象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 獲取所有已注冊(cè)的類
    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);
}

從控制臺(tái)輸出的情況可以看出KVO生成的中間類是別被觀察對(duì)象類的派生子類待榔。

KVO產(chǎn)生的中間類是繼承還是重寫(xiě)屬性的setter方法

打印出NSKVONotifying_NAPerson類中的方法

[self printClassAllMethod:objc_getClass("NSKVONotifying_NAPerson")];

#pragma mark - 遍歷當(dāng)前類的方法
- (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);
}

輸出:
setName:-0x1807930a4
class-0x180791b78
dealloc-0x1807918fc
_isKVOA-0x1807918f4

由于NSKVONotifying_NAPerson類中存在setName方法逞壁,因此KVO子類重寫(xiě)了觀察屬性的setter方法。

對(duì)象的isa什么時(shí)候指回NAPerson

在移除觀察者前后斷點(diǎn)打印類名可知移除觀察者后被觀察對(duì)象的isa指回了原類

盡管移除了觀察者锐锣,但是KVO產(chǎn)生的派生子類并不會(huì)移除腌闯,依然存在于內(nèi)存中。因?yàn)楫?dāng)前界面銷毀后通過(guò)上一個(gè)界面打印NAPerson類以及子類(printClasses:)依然會(huì)出現(xiàn)NSKVONotifying_NAPerson雕憔。

KVO產(chǎn)生的派生子類重寫(xiě)的setter做了什么操作

添加斷點(diǎn):

修改name進(jìn)入斷點(diǎn):

進(jìn)入?yún)R編中可以看到調(diào)用父類setName:方法前后調(diào)用了NSKeyValueWillChange姿骏、NSKeyValueDidChange


四、總結(jié)

基本原理:
  1. KVO是基于Runtime機(jī)制實(shí)現(xiàn)的
  2. 當(dāng)某個(gè)類的對(duì)象屬性第一次被觀察時(shí)斤彼,系統(tǒng)就會(huì)在運(yùn)行期動(dòng)態(tài)地創(chuàng)建該類的一個(gè)派生類分瘦,在這個(gè)派生類中重寫(xiě)基類中任何被觀察屬性的setter方法。派生類在被重寫(xiě)的setter方法內(nèi)實(shí)現(xiàn)真正的通知機(jī)制
  3. 如果原類為Person畅卓,那么生成的派生類名為NSKVONotifying_Person
  4. 每個(gè)類對(duì)象中都有一個(gè)isa指針指向當(dāng)前類擅腰,當(dāng)一個(gè)類對(duì)象的第一次被觀察,那么系統(tǒng)就會(huì)偷偷將isa指針指向動(dòng)態(tài)生成的派生類翁潘,從而在給被監(jiān)控屬性賦值是執(zhí)行的是派生類的setter方法
  5. 鍵值觀察通知依賴于NSObject的兩個(gè)方法:willChangeValueForKey:didChangeValueForKey:,在一個(gè)被觀察屬性發(fā)生改變之前趁冈,willChangeValueForKey:一定會(huì)被調(diào)用,這就會(huì)記錄舊的值。而當(dāng)改變發(fā)生后渗勘,didChangeValueForKey:會(huì)被調(diào)用沐绒,繼而observeValueForKey:ofObject:change:context:也會(huì)被調(diào)用

關(guān)于KVO中間類,有如下說(shuō)明:

  • 實(shí)例對(duì)象isa的指向在注冊(cè)KVO觀察者之后旺坠,由原有類更改為指向中間類
  • 中間類重寫(xiě)了觀察屬性的setter方法乔遮、class、dealloc取刃、_isKVOA方法
  • dealloc方法中蹋肮,移除KVO觀察者之后,實(shí)例對(duì)象isa指向由中間類更改為原類
  • 中間類從創(chuàng)建后璧疗,就一直存在內(nèi)存中坯辩,不會(huì)被銷毀
KVO為子類的觀察者屬性重寫(xiě)的setter方法的工作原理相當(dāng)于:
- ( void)setName:( NSString *)name {
  [self willChangeValueForKey: @"name"];

  [super setValue:name forKey: @"name"]; //調(diào)用父類的存取方法 

  [self didChangeValueForKey: @"name"];
}
特點(diǎn):
  • 觀察者觀察的是屬性,只有遵循KVO變更屬性值的方式才會(huì)執(zhí)行KVO的回調(diào)方法崩侠,例如是否執(zhí)行了setter方法漆魔、或者是否使用了KVC賦值。

  • 如果賦值沒(méi)有通過(guò)setter方法或者KVC却音,而是直接修改屬性對(duì)應(yīng)的成員變量改抡,例如:僅調(diào)用_name = @"newName",這時(shí)是不會(huì)觸發(fā)KVO機(jī)制系瓢,更加不會(huì)調(diào)用回調(diào)方法的阿纤。

  • 所以使用KVO機(jī)制的前提是遵循KVO的屬性賦值方式來(lái)變更屬性值。


參考

Advancements in the Objective-C runtime
ro八拱、rw、rwe介紹:iOS-底層探索13:分類的加載
KVO實(shí)現(xiàn)原理和具體應(yīng)用
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末肌稻,一起剝皮案震驚了整個(gè)濱河市清蚀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌枷邪,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诺凡,死亡現(xiàn)場(chǎng)離奇詭異东揣,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)腹泌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)嘶卧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人凉袱,你說(shuō)我怎么就攤上這事芥吟≌焱” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵钟鸵,是天一觀的道長(zhǎng)钉稍。 經(jīng)常有香客問(wèn)我,道長(zhǎng)棺耍,這世上最難降的妖魔是什么贡未? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮蒙袍,結(jié)果婚禮上俊卤,老公的妹妹穿的比我還像新娘。我一直安慰自己害幅,他們只是感情好瘾蛋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著矫限,像睡著了一般。 火紅的嫁衣襯著肌膚如雪佩抹。 梳的紋絲不亂的頭發(fā)上叼风,一...
    開(kāi)封第一講書(shū)人閱讀 51,488評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音棍苹,去河邊找鬼无宿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛枢里,可吹牛的內(nèi)容都是我干的孽鸡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼栏豺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼彬碱!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起奥洼,我...
    開(kāi)封第一講書(shū)人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤巷疼,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后灵奖,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體嚼沿,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年瓷患,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了骡尽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡擅编,死狀恐怖攀细,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤辨图,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布班套,位于F島的核電站,受9級(jí)特大地震影響故河,放射性物質(zhì)發(fā)生泄漏吱韭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一鱼的、第九天 我趴在偏房一處隱蔽的房頂上張望理盆。 院中可真熱鬧,春花似錦凑阶、人聲如沸猿规。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)姨俩。三九已至,卻和暖如春师郑,著一層夾襖步出監(jiān)牢的瞬間环葵,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工宝冕, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留张遭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓地梨,卻偏偏與公主長(zhǎng)得像菊卷,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宝剖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

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