iOS 通知、KVC厂财、KVO 原理和實(shí)現(xiàn)機(jī)制剖析

一芋簿、通知

通知是一種一對(duì)多的信息廣播機(jī)制,與 delegate 和 block 的區(qū)別是:通知是一對(duì)多傳遞璃饱,delegate 和 block 是一對(duì)一的傳遞与斤。

由于 OC 的閉源,我們無法確切的知道通知具體的實(shí)現(xiàn)機(jī)制,但是如果是我們自己會(huì)怎樣實(shí)現(xiàn)通知的機(jī)制呢撩穿?下面是我的看法:

我們?cè)谔砑右粋€(gè)通知時(shí)往往會(huì)在一開始調(diào)用 [NSNotificationCenter.defaultCenter addObserver: selector: name: object:]方法這時(shí)會(huì)發(fā)生什么磷支?

// 一個(gè)全局的 manager 來管理通知的接收和發(fā)送機(jī)制
class NotificationManager {
    static NotificationHashMap *_map; 
};
// 一個(gè)哈希表,通過哈希算法查以通知 name 作為鍵值來查找通知的接收對(duì)象和執(zhí)行方法
struct NotificationHashMap {
    struct NotificationMap * _Nonnull * _Nonnull _map;
};
// name: 通知名稱
// value: 可以看成一個(gè)二維數(shù)組食寡,里面一層存放的是 
// 1.接收對(duì)象(observer) 2.執(zhí)行方法(sel) 3.對(duì)象(object)
struct NotificationMap {
    const char * _Nonnull name;
    struct NotificationValueList * _Nonnull * _Nonnull value;
};
// observer: 接收對(duì)象
// sel: 執(zhí)行方法
// object: 對(duì)象
struct NotificationValueList {
    id observer;
    SEL sel;
    id object;
};

首先系統(tǒng)會(huì)創(chuàng)建一個(gè) NotificationValueList 實(shí)例雾狈,通過 NotificationManager 和 name 查找到 NotificationHashMap 中對(duì)應(yīng)的 value,將剛才創(chuàng)建的 NotificationValueList 實(shí)例添加到 value 中抵皱。這里查找 value 的方式為哈希算法善榛,value 類似一個(gè)集合,不會(huì)添加相同的實(shí)例(observer, sel, object都相同的不會(huì)再次添加)

在這里我們會(huì)將 observer 和 object 兩個(gè)對(duì)象添加到全局變量中呻畸,但是系統(tǒng)不會(huì)為它們的引用計(jì)數(shù)加 1移盆。否則對(duì)象將無法被釋放。這也是為什么在對(duì)象的 - dealloc 中我們要調(diào)用 [NSNotificationCenter.defaultCenter removeObserver: name: object:]方法伤为。因?yàn)槲覀冃枰?name 對(duì)應(yīng)的 value 中的相應(yīng)NotificationValueList 實(shí)例刪除咒循。

我們發(fā)送通知的時(shí)候會(huì)調(diào)用這個(gè)方法:[NSNotificationCenter.defaultCenter postNotificationName: object: userInfo:],它會(huì)通過 name 找到 NotificationHashMap 中的 value绞愚,然后遍歷 value 集合叙甸,判斷每個(gè)NotificationValueList 實(shí)例的 object 是否與發(fā)送消息方法中的 object 參數(shù)相同,如果相同位衩,就會(huì)找到該實(shí)例的 observer蚁署,調(diào)用 sel 方法,如果 sel 方法帶有 NSNotification 參數(shù)蚂四,就把 userInfo 傳遞過去光戈。

以上就是我自己對(duì)通知實(shí)現(xiàn)機(jī)制的理解,系統(tǒng)肯定不是這樣實(shí)現(xiàn)的遂赠,但是我們不得而知久妆。

二、KVC

KVC 全稱 Key Valued Coding(鍵值編碼)跷睦,是基于 NSKeyValueCoding 非正式協(xié)議實(shí)現(xiàn)的機(jī)制筷弦,它可以在運(yùn)行時(shí)通過 key 值對(duì)對(duì)象的屬性動(dòng)態(tài)的進(jìn)行存取操作。

主要方法有:
valueForKey:
setValue: forKey:

原理探究

為了探究 KVC 的系統(tǒng)實(shí)現(xiàn)機(jī)制請(qǐng)看下面代碼:

@interface KVCTestObject : NSObject

@end

@implementation KVCTestObject {
    NSString *_test;
    NSString *_isTest;
    NSString *test;
    NSString *isTest;
}

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

#pragma mark - 重寫 KVC 方法
- (void)setValue:(id)value forKey:(NSString *)key {
    [super setValue:value forKey:key];
    if (![key isEqualToString:@"test"]) {
        return;
    }
    NSLog(@"_test: %@, test: %@, _isTest: %@, isTest: %@", _test, test, _isTest, isTest);
}

- (id)valueForKey:(NSString *)key {
    id value = [super valueForKey:key];
    if (![key isEqualToString:@"test"]) {
        return value;
    }
    NSLog(@"_test: %@, test: %@, _isTest: %@, isTest: %@", _test, test, _isTest, isTest);
    return value;
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"valueForUndefinedKey:");
    return nil;
//    return [super valueForUndefinedKey:key];
}

#pragma mark - set
- (void)setTest:(NSString *)test {
    NSLog(@"setTest:");
}

- (void)_setTest:(NSString *)test {
    NSLog(@"_setTest:");
}
- (void)setIsTest:(NSString *)isTest {
    NSLog(@"setIsTest:");
}
- (void)_setIsTest:(NSString *)isTest {
    NSLog(@"_setIsTest:");
}

#pragma mark - get
- (NSString *)getTest {
    NSLog(@"getTest");
    return @"";
}

- (NSString *)test {
    NSLog(@"test");
    return @"";
}

- (NSString *)isTest {
    NSLog(@"isTest");
    return @"";
}

- (NSString *)_getTest {
    NSLog(@"_getTest");
    return @"";
}

- (NSString *)_test {
    NSLog(@"_test");
    return @"";
}

- (NSString *)_isTest {
    NSLog(@"_isTest");
    return @"";
}
- (void)viewDidLoad {
    [super viewDidLoad];
    KVCTestObject *kvc = [KVCTestObject new];
    [kvc setValue:@"2" forKey:@"test"];
    [kvc valueForKey:@"test"];
}

結(jié)論

通過上面的代碼抑诸,每次把走的 set 和 get 方法注釋掉烂琴,可以知道 KVC 賦值和取值查找的方法和優(yōu)先級(jí)。

通過試驗(yàn)得出以下結(jié)論:

  1. 賦值(setValue: forKey:):查找順序:
    第一步:setTest: -> _setTest: -> setIsTest:蜕乡。而_setIsTest:沒有任何作用前三個(gè)都沒有也不會(huì)走奸绷。
    第二步:+ (BOOL)accessInstanceVariablesDirectly 方法如果返回 YES 則會(huì)賦值 _test->_isTest->test->isTest實(shí)例變量,如果為 NO层玲,到第三步号醉。
    第三步:找不到會(huì)走- (void)setValue: forUndefinedKey:方法反症,拋出異常,我們可以重寫該方法使其不去拋出異常畔派。一般配合 2 中的第三步使用铅碍。

KVC Set 查找原理

  1. 取值(valueForKey: ):查找順序是:
    第一步:getTest: -> test: -> isTest: -> _getTest: -> _test:。而_isTest:沒有任何作用前五個(gè)都沒有也不會(huì)走线椰。
    第二步:+ (BOOL)accessInstanceVariablesDirectly 方法如果返回 YES 則會(huì)取值 _test->_isTest->test->isTest實(shí)例變量胞谈,如果為 NO,到第三步憨愉。
    第三步:找不到會(huì)走- (id)valueForUndefinedKey:方法烦绳,拋出異常,我們可以重寫該方法返回 nil 使其不去拋出異常莱衩。

KVC Get 查找原理.jpg

理解了 KVC 的原理我們就可以很容易理解我們?cè)陂_發(fā)中常用的解析 json 后為 mode 批量賦值的方法爵嗅。也明白為什么要在 BaseModel 中重寫 - (void)setValue: forUndefinedKey:- (id)valueForUndefinedKey: 方法了娇澎。

// KVC 的批量取值和賦值
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

三笨蚁、KVO

KVO 是 Key-Value Observing 的簡(jiǎn)寫,它是 OC 系統(tǒng)實(shí)現(xiàn)觀察者模式的方式趟庄。當(dāng)指定對(duì)象的屬性被修改括细,就會(huì)通知觀察者,告訴觀察者相應(yīng)對(duì)象的相應(yīng)屬性被修改戚啥。

系統(tǒng)如何實(shí)現(xiàn) KVO奋单?

@interface KVOTestObject : NSObject

@property (nonatomic, copy) NSString *test;

@end
- (void)viewDidLoad {
    [super viewDidLoad];
    KVOTestObject *kvoObj = [KVOTestObject new];
    NSLog(@"%s", class_getName(object_getClass(kvoObj)));
    [kvoObj addObserver:self forKeyPath:@"test" options:(NSKeyValueObservingOptionNew) context:nil];
    NSLog(@"%s", class_getName(object_getClass(kvoObj)));
}

我們看看 - addObserver:self forKeyPath: options: context: 方法做了什么,看上面的代碼猫十,兩條打印日志是什么览濒?結(jié)果是:

KVOTestObject
NSKVONotifying_KVOTestObject

我們可以看到在添加鍵值觀察之前,kvoObj 的類是 KVOTestObject拖云,這和我們的定義一樣贷笛,但是添加鍵值觀察后,kvoObjc 的類變成了 NSKVONotifying_KVOTestObject宙项。其實(shí)NSKVONotifying_KVOTestObjectKVOTestObject的子類乏苦。

當(dāng)我們?yōu)橐粋€(gè)類 A 添加一個(gè)鍵值觀察時(shí),系統(tǒng)會(huì)自動(dòng)創(chuàng)建一個(gè) NSKVONotifying_A 類尤筐,繼承類 A汇荐,然后 將kvoObjc 的 isa 指針指向 NSKVONotifying_A。 通過 重寫鍵值屬性的 set 方法 的形式來實(shí)現(xiàn) KVO 觀察者模式

@interface KVOTestObject : NSObject

@property (nonatomic, copy) NSString *test;

- (void)changeTest:(NSString *)test;

@end

@implementation KVOTestObject

- (void)changeTest:(NSString *)test {
    _test = test.copy;
}

@end
- (void)viewDidLoad {
    [super viewDidLoad];
    KVOTestObject *kvoObj = [KVOTestObject new];
    [kvoObj addObserver:self forKeyPath:@"test" options:(NSKeyValueObservingOptionNew) context:nil];
    kvoObj.test = @"1";
    [kvoObj changeTest:@"2"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"test"] && [object isKindOfClass:KVOTestObject.class]) {
        NSLog(@"newKey:%@", change[NSKeyValueChangeNewKey]);
    }
}

請(qǐng)看上面的代碼會(huì)打印幾條日志?

newKey:1

答案是只會(huì)打印一條日志腺劣,因?yàn)?KVO 實(shí)現(xiàn)的機(jī)制是重寫 set 方法曾棕,而 - changeTest: 方法直接為實(shí)例變量賦值,沒有走 set 方法繁疤,故而不會(huì)響應(yīng) KVO咖为。

我們可以通過手動(dòng) KVO 的形式強(qiáng)行通知觀察者響應(yīng) - observeValueForKeyPath ofObject: change: context 方法,方式如下:

@implementation KVOTestObject
- (void)changeTest:(NSString *)test {
    [self willChangeValueForKey:@"test"];
    _test = test.copy;
    [self didChangeValueForKey:@"test"];
}
@end

打印日志變?yōu)椋?/p>

newKey:1
newKey:2
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末稠腊,一起剝皮案震驚了整個(gè)濱河市躁染,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌架忌,老刑警劉巖吞彤,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異叹放,居然都是意外死亡饰恕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門井仰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來埋嵌,“玉大人,你說我怎么就攤上這事俱恶”⑧拢” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵合是,是天一觀的道長(zhǎng)了罪。 經(jīng)常有香客問我,道長(zhǎng)聪全,這世上最難降的妖魔是什么泊藕? 我笑而不...
    開封第一講書人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮难礼,結(jié)果婚禮上娃圆,老公的妹妹穿的比我還像新娘。我一直安慰自己蛾茉,他們只是感情好讼呢,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著臀稚,像睡著了一般吝岭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吧寺,一...
    開封第一講書人閱讀 50,050評(píng)論 1 291
  • 那天窜管,我揣著相機(jī)與錄音,去河邊找鬼稚机。 笑死幕帆,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赖条。 我是一名探鬼主播失乾,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼常熙,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了碱茁?” 一聲冷哼從身側(cè)響起裸卫,我...
    開封第一講書人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎纽竣,沒想到半個(gè)月后墓贿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蜓氨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年聋袋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片穴吹。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡幽勒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出港令,到底是詐尸還是另有隱情啥容,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布缠借,位于F島的核電站干毅,受9級(jí)特大地震影響宜猜,放射性物質(zhì)發(fā)生泄漏泼返。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一姨拥、第九天 我趴在偏房一處隱蔽的房頂上張望绅喉。 院中可真熱鬧,春花似錦叫乌、人聲如沸柴罐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽革屠。三九已至,卻和暖如春排宰,著一層夾襖步出監(jiān)牢的瞬間似芝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來泰國打工板甘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留党瓮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓盐类,卻偏偏與公主長(zhǎng)得像寞奸,于是被迫代替她去往敵國和親呛谜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

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