Runtime窺探 (五)| KVO底層實(shí)現(xiàn)

前言

怎么看待勵(lì)志的書籍?
看再多糖荒,那都是別人的人生

踏實(shí)走自己的路

一捶朵、KVO介紹

KVO(鍵值監(jiān)聽 Key-Value Observing),是OC觀察者設(shè)計(jì)模式的一種具體實(shí)現(xiàn)硼端。

作用:當(dāng)指定的對(duì)象的屬性被修改后,觀察者就會(huì)接受到監(jiān)聽通知的消息寓搬,開發(fā)者可以根據(jù)收到消息來進(jìn)行自定義處理句喷。

二、KVO使用

KVO的使用步驟大概分為三步:

  • 1.添加觀察者兄春,實(shí)施監(jiān)聽
  • 2.監(jiān)聽方法中處理屬性發(fā)生的變化
  • 3.移除觀察者

具體例子如下:

#pragma mark - ------------runtime在KVO中的使用--------------
- (void)objc_KVO{
    self.num = 0;
    self.p = [[Person alloc] init];
    self.p.name = @"firstName";
    //添加觀察者
    [self.p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionInitial context:NULL];
//    [self.p test_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}

//監(jiān)聽方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@-%@-%@", keyPath, change,self.p);
}

//移除監(jiān)聽
- (void)dealloc{
    [self.p removeObserver:self forKeyPath:@"name"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.num++;
    self.p.name = [NSString stringWithFormat:@"name%ld",self.num];
}

三赶舆、KVO的底層原理

Apple官方文檔

Apple 的文檔有簡單提到過 KVO 的實(shí)現(xiàn):

Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...

上面大概的意思就是:被觀察對(duì)象的 isa 指針會(huì)指向一個(gè)中間類芜茵,而不是原來真正的類九串。Apple并沒有說明具體的實(shí)現(xiàn)細(xì)節(jié)寺鸥。那我們就來嘗試猜測KVO的底層實(shí)現(xiàn)原理胆建。

驗(yàn)證原理

1.生成中間類NSKVONotifying_XXX

對(duì)上面的例子加4個(gè)斷點(diǎn):分別在添加觀察者之前、之后扑馁、以及監(jiān)聽方法的地方。

斷點(diǎn)

根據(jù)上面的斷點(diǎn)處沿侈,每個(gè)斷點(diǎn)打印出被觀察的類以及類的isa指針市栗,結(jié)果如下:

斷掉打印

可以看出在我們添加觀察者之后,被觀察的類的isa變成了NSKVONotifying_Person蛛淋,也就是說我們的類person變成了另外一個(gè)類褐荷。其實(shí)這個(gè)NSKVONotifying_XXX類是我們被觀察者的子類,是動(dòng)態(tài)創(chuàng)建的层宫。
那什么時(shí)候我們的類會(huì)變成原始的類呢萌腿?那肯定是我們調(diào)用removeObserver之后抖苦,就會(huì)變成原始的類。

這也驗(yàn)證了Apple官方文檔中贮庞,生成了一個(gè)中間類:NSKVONotifying_XXX

2.KVO只監(jiān)聽setter方法

我們正常定義一個(gè)屬性如下贸伐,系統(tǒng)會(huì)自動(dòng)生成一個(gè)setNamegetName的方法怔揩,

@property (nonatomic, strong) NSString *name;

現(xiàn)在我們自定義set方法

@property (nonatomic, strong, setter=setname:) NSString *name;

//實(shí)現(xiàn)
- (void)setname:(NSString *)name{
    _name = name;
}

注意這里的方法setname不是標(biāo)準(zhǔn)命名的set方法商膊,當(dāng)我們點(diǎn)擊屏幕的時(shí)候修改name的時(shí)候,監(jiān)聽方法并沒有執(zhí)行藐翎。而當(dāng)我們是正常的setName方法吝镣,監(jiān)聽方法就會(huì)執(zhí)行昆庇。同樣的方法驗(yàn)證getter方法,發(fā)現(xiàn)跟getter方法沒有關(guān)系拱撵。

也就是說KVO監(jiān)聽過程是通過setter方法來操作的。也就是說如果的setter方法命名不標(biāo)準(zhǔn)(set首字母大寫)或者沒有實(shí)現(xiàn)乓旗,那么KVO監(jiān)聽則無效集索。

3.KVO重寫了class方法

上面的斷點(diǎn)中抄谐,添加觀察者之前是person類,添加觀察者之后是NSKVONotifying_Person類毅厚。但是我們打印self.p卻發(fā)現(xiàn)還是打印出來person而不是NSKVONotifying_Person吸耿,其中的isa是中間類酷窥。這個(gè)是蘋果沒法沒有隱藏的,才是真正的類妆棒。name只是蘋果重寫了class方法糕珊,把class中結(jié)構(gòu)體的name指定為先前的類名而已毅糟,為了隱藏中間的類,返回還是真正的中間類喇肋。當(dāng)我們r(jià)emoveobserve后蝶防,isa指針又會(huì)指向我們原始的類明吩。把中間類銷毀了。

KVO的原理說明

當(dāng)我們添加KVO監(jiān)聽對(duì)象A時(shí)菱鸥,KVO機(jī)制動(dòng)態(tài)創(chuàng)建一個(gè)對(duì)象A所屬類的子類(NSKVONotifying_A)氮采,并且為這個(gè)子類動(dòng)態(tài)添加一個(gè)被觀察屬性keyPath的setter方法染苛,然后把A所屬類的isa指針指向新建子類茶行,所以當(dāng)我們調(diào)用A所屬類的setter方法其實(shí)調(diào)用的是新建子類的setter方法,setter方法隨后負(fù)責(zé)通知觀察對(duì)象屬性的改變狀況以及調(diào)用A所屬類的setter方法娶靡。

自定義模擬KVO底層實(shí)現(xiàn)

不關(guān)注KVO方法中后兩個(gè)參數(shù)姿锭,只是一些if和else填充的回調(diào)參數(shù)伯铣,這些就是一些細(xì)節(jié)考慮,這里只是關(guān)注和模擬kvo的流程實(shí)現(xiàn)焚鲜,不考慮一些容錯(cuò)處理忿磅,只關(guān)注原理實(shí)現(xiàn)犀斋,學(xué)習(xí)這種思想叽粹,知道kvo底層是如何運(yùn)轉(zhuǎn)起來的就可以了,我們沒必要重寫一個(gè)kvo機(jī)制锤灿,因?yàn)樘O果把這個(gè)封裝的很好了但校。當(dāng)然有興趣也可以嘗試實(shí)現(xiàn)一個(gè)完整的kvo啡氢。下面代碼基本上可以把整個(gè)流程寫清楚了术裸,看懂下面就差不多了袭艺。偽代碼我就不寫了猾编。

#import "NSObject+KVO.h"
#import <objc/message.h>

NSString *const kJYKVOClassPrefix = @"JYKVOClassPrefix_";//自定義類前綴
NSString *const kJYKVOAssociatedParameDict = @"JYKVOAssociatedParameDict";//參數(shù)key
NSString *const kJYKVOObservers = @"JYKVOObservers";  //觀察者key
NSString *const kJYKVOOldValue = @"JYKVOOldValue";   //舊值key
NSString *const kJYKVOKeyPath = @"JYKVOKeyPath";     //KeyPathkey
NSString *const kJYKVOSetter = @"JYKVOSetter";      //setter方法key

@implementation NSObject (KVO)

//添加觀察者
//下面采用面向過程編程方式答倡,所以下面這個(gè)函數(shù)有點(diǎn)長驴党,只是為了讓大家看清整個(gè)流程,
- (void)test_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    
    //獲取self所屬的類
    Class originalClass = object_getClass(self);
    NSString *originalClassStr = NSStringFromClass(originalClass);
    
    //獲取setter方法字符串
    NSString *setter = [NSString stringWithFormat:@"set%@:",[keyPath capitalizedString]];
    
    //1.判斷被觀察者(self)對(duì)應(yīng)的keyPath有沒有setter方法,沒有則返回,添加觀察者失敗
    BOOL haveSetter = NO;
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList(originalClass, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        NSString *methodName = NSStringFromSelector(method_getName(methodList[i]));
        if ([methodName isEqualToString:setter]) {
            haveSetter = YES;
            break;
        }
    }
    free(methodList);
    if (!haveSetter) {
        return;
    }
    
    //2.動(dòng)態(tài)生成一個(gè)前綴為JYKVOClassPrefix_的類,這個(gè)新類是self的子類
    NSString *newClassStr = [NSString stringWithFormat:@"%@%@",kJYKVOClassPrefix,originalClassStr];
    Class  newClass = objc_allocateClassPair(originalClass, newClassStr.UTF8String, 0);
    
    //3.為新類添加setter方法
    class_addMethod(newClass, NSSelectorFromString(setter), (IMP)newClassSetterMethod, "v@:@");
    //方法屬性添加完成后注冊這個(gè)類才算創(chuàng)建成功可以使用
    objc_registerClassPair(newClass);
    
    //4.修改被觀察者(self)的isa指針攘轩,讓isa指針指向新建的子類度帮。也就是說被觀察者(self)現(xiàn)在所屬于的類是新建的子類,
    object_setClass(self, newClass);
    
    //5.使用關(guān)聯(lián)值保存信息
    NSMutableDictionary *parameDict = objc_getAssociatedObject(self, (__bridge const void *)(kJYKVOAssociatedParameDict));
    if (!parameDict) {
        parameDict = [NSMutableDictionary dictionary];
    }
    if (keyPath) {
        [parameDict setValue:setter forKey:kJYKVOSetter];
        [parameDict setValue:keyPath forKey:kJYKVOKeyPath];
    }
    //kvc獲取舊值
    id oldValue = [self valueForKeyPath:keyPath];
    if (oldValue) {
        [parameDict setValue:oldValue forKey:kJYKVOOldValue];
    }

    //觀察者數(shù)組
    if ([parameDict objectForKey:kJYKVOObservers] != nil) {
        NSMutableArray *observers = [parameDict objectForKey:kJYKVOObservers];
        [observers addObject:observer];
        [parameDict setValue:observers forKey:kJYKVOObservers];
    }else{
        NSMutableArray *observers = [NSMutableArray array];
        [observers addObject:observer];
        [parameDict setValue:observers forKey:kJYKVOObservers];
    }
    //關(guān)聯(lián)
    objc_setAssociatedObject(self, (__bridge const void *)kJYKVOAssociatedParameDict, parameDict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}


//新類setter方法的實(shí)現(xiàn)
static void newClassSetterMethod(id self, SEL _cmd, id newValue){
    
    //1.設(shè)置當(dāng)前子類指向父類(原始的類)
    //獲取當(dāng)前類,這個(gè)class就是我們新建的子類
    Class newClass = object_getClass(self);
    
    //獲取當(dāng)前類的父類,也就是我們最原始的類
    Class superClass = class_getSuperclass(newClass);
    
    //把當(dāng)前子類設(shè)置為父類率翅,也就是設(shè)置成我們原始的類,讓我們原始的類來調(diào)用setter方法腺晾,這樣就正常的調(diào)用我們原始的setter方法
    object_setClass(self, superClass);
    
    
    //2.調(diào)用父類的setter方法
    //獲取關(guān)聯(lián)的參數(shù)
    NSDictionary *parameDict = objc_getAssociatedObject(self, (__bridge const void *)(kJYKVOAssociatedParameDict));
    NSString *setter = [parameDict objectForKey:kJYKVOSetter];
    //消息發(fā)送調(diào)用setter方法
    objc_msgSend(self, NSSelectorFromString(setter), newValue);
    
    NSMutableArray *observers = [parameDict objectForKey:kJYKVOObservers];
    NSString *keyPath = [parameDict objectForKey:kJYKVOKeyPath];
    NSMutableDictionary<NSKeyValueChangeKey, id> *change = [NSMutableDictionary dictionary];
    
    change[NSKeyValueChangeNewKey] = newValue;
    change[NSKeyValueChangeOldKey] = [parameDict objectForKey:kJYKVOOldValue] != nil?[parameDict objectForKey:kJYKVOOldValue]:NULL;

    for (NSObject *observer in observers) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),keyPath, self, change, NULL);
        });
    }
    
    object_setClass(self, newClass);
    [parameDict setValue:newValue forKey:kJYKVOOldValue];
    objc_setAssociatedObject(self, (__bridge const void *)kJYKVOAssociatedParameDict, parameDict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
}

//+ (Class)class{
//
//    Class cls = object_getClass(self);
//    
//    cls->name = "截取原始類名";//這里蘋果不允許我們修改悯蝉,runtime源碼中可以查看和修改
//    return self;
//}
@end

四鼻由、KVO的總結(jié)

通過上面我們知道KVO的底層原理,同時(shí)這種設(shè)計(jì)思想也值得我們學(xué)習(xí)蔼紧,通過中間類來偽裝很是巧妙歉井。希望根據(jù)這種思想來解決我們實(shí)際項(xiàng)目中的問題哈误。后面要講解的AOP編程的開源庫Aspect也是使用這種思想蜜自。期待你們能模仿一些思想寫出優(yōu)秀的開源庫卢佣。。只能說Runtime太強(qiáng)大啦

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末戈鲁,一起剝皮案震驚了整個(gè)濱河市婆殿,隨后出現(xiàn)的幾起案子罩扇,更是在濱河造成了極大的恐慌喂饥,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件或粮,死亡現(xiàn)場離奇詭異氯材,居然都是意外死亡硝岗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門命浴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來生闲,“玉大人,你說我怎么就攤上這事悬蔽∽叫耍” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵禾乘,是天一觀的道長始藕。 經(jīng)常有香客問我氮趋,道長,這世上最難降的妖魔是什么诉植? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任倍踪,我火速辦了婚禮建车,結(jié)果婚禮上椒惨,老公的妹妹穿的比我還像新娘。我一直安慰自己领斥,他們只是感情好沃暗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布孽锥。 她就那樣靜靜地躺著细层,像睡著了一般疫赎。 火紅的嫁衣襯著肌膚如雪碎节。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天胎撇,我揣著相機(jī)與錄音晚树,去河邊找鬼受葛。 笑死总滩,一個(gè)胖子當(dāng)著我的面吹牛巡雨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冈涧,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼督弓,長吁一口氣:“原來是場噩夢啊……” “哼乒验!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起狂塘,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤鳄厌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后泪漂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赘风,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年邀窃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了假哎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肪虎,死狀恐怖扇救,靈堂內(nèi)的尸體忽然破棺而出香嗓,到底是詐尸還是另有隱情,我是刑警寧澤沧烈,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布像云,位于F島的核電站,受9級(jí)特大地震影響腋逆,放射性物質(zhì)發(fā)生泄漏侈贷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一柬泽、第九天 我趴在偏房一處隱蔽的房頂上張望锨并。 院中可真熱鬧睬棚,春花似錦解幼、人聲如沸包警。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鲫剿。三九已至稻轨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間政冻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工明场, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榕堰,地道東北人竖慧。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓圾旨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親痹筛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子廓鞠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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

  • 一、概述 KVO滋早,即:Key-Value Observing砌们,它提供一種機(jī)制搁进,當(dāng)指定的對(duì)象的屬性被修改后,則其觀察...
    DeerRun閱讀 10,070評(píng)論 11 33
  • 本篇會(huì)對(duì)KVO的實(shí)現(xiàn)進(jìn)行探究饼问,不涉及太多KVO的使用方法揭斧,但是會(huì)有一些使用時(shí)的思考。 一驮吱、使用上的疑問 1.key...
    奮拓達(dá)閱讀 508評(píng)論 0 2
  • 上半年有段時(shí)間做了一個(gè)項(xiàng)目萧吠,項(xiàng)目中聊天界面用到了音頻播放,涉及到進(jìn)度條纸型,當(dāng)時(shí)做android時(shí)候處理的不太好,由于...
    DaZenD閱讀 3,018評(píng)論 0 26
  • 前言 Key-Value-Observer,它來源于觀察者模式, 其基本思想(copy于某度)是一個(gè)目標(biāo)對(duì)象管理所...
    CholMay閱讀 3,400評(píng)論 6 18
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,721評(píng)論 0 9