KVO詳解及底層實(shí)現(xiàn)

什么是KVO??

KVO就是NSKeyValueObserving曙求,請看官方文檔的解釋:

image

大概翻譯如下:

一種非正式協(xié)議,通知其他對象的指定屬性發(fā)生了改變排监。

簡單理解就是可岂,可以監(jiān)聽一個對象的某個屬性是否發(fā)生改變陡蝇。

那么問題來了旗吁,什么是非正式協(xié)議踩萎??有正式協(xié)議嗎很钓?香府?

麻蛋董栽,本來想找官方文檔的,找了半天沒找到企孩。從Stackoverflow找到了答案锭碳,貌似原來官方文檔的鏈接失效了

image

大概翻譯如下:

非正式協(xié)議:非正式協(xié)議是NSObject的一個類別Category,幾乎所有的對象都隱含的采用(類別是OC的語言特性柠硕,能夠給類對象添加方法而不需要創(chuàng)建子類)工禾,非正式協(xié)議的方法是可選的

正式協(xié)議: 一個正式協(xié)議聲明了類需要實(shí)現(xiàn)的方法列表运提,正式協(xié)議有自己的聲明蝗柔、采用和類型檢查語法。你可以使用@required或者optional關(guān)鍵字指定方法是否必須實(shí)現(xiàn)民泵。子類繼承父類采用的協(xié)議癣丧。正式協(xié)議也可以遵守其他協(xié)議

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

  • 監(jiān)聽某個對象的某個屬性
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
  • 實(shí)現(xiàn)非正式協(xié)議
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
  • 移除監(jiān)聽
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

簡單代碼演示:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    self.person = [[ZJPerson alloc] init];
    
    [self.person setName:@"zhangsan"];
    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person setName:@"lisi"];
}

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

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name"];
}

打印結(jié)果:


image

用法其實(shí)很簡單,接下來重點(diǎn)來了栈妆,KVO為什么能夠監(jiān)聽到屬性變化胁编,底層做了什么?鳞尔?

KVO底層實(shí)現(xiàn)探究

首先嬉橙,我們利用runtime在添加監(jiān)聽之前和之后分別打印一下類對象

NSLog(@"%@", object_getClass(self.person));
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
NSLog(@"%@", object_getClass(self.person));

打印結(jié)果:

2018-05-19 22:48:18.726028+0800 KVO[33804:3059947] ZJPerson
2018-05-19 22:48:18.726535+0800 KVO[33804:3059947] NSKVONotifying_ZJPerson

我們發(fā)現(xiàn)添加監(jiān)聽之后,實(shí)例對象的類對象發(fā)生了變化寥假,系統(tǒng)為我們動態(tài)添加了一個NSKVONotifying_+類名的類市框,因?yàn)槲覀兏淖儗ο髮傩缘闹凳峭ㄟ^setter方法實(shí)現(xiàn)了,所以很明顯是系統(tǒng)動態(tài)生成的NSKVONotifying_ZJPerson類重寫了setter方法糕韧。不信的話枫振,我們可以做一個實(shí)驗(yàn),自己手動添加一個NSKVONotifying_ZJPerson類萤彩,看下會打印什么

2018-05-19 22:56:32.223288+0800 KVO[33919:3068985] [general] KVO failed to allocate class pair for name NSKVONotifying_ZJPerson, automatic key-value observing will not work for this class

錯誤提示很明顯粪滤,告訴我們創(chuàng)建NSKVONotifying_ZJPerson類失敗,KVO失效

那么系統(tǒng)自動創(chuàng)建重寫的的setter方法內(nèi)部做了什么呢雀扶?杖小?同樣在添加監(jiān)聽方法之前,利用runtime打印下方法的實(shí)現(xiàn),截圖如下:

image

發(fā)現(xiàn)方法實(shí)現(xiàn)變了愚墓,內(nèi)部調(diào)用了系統(tǒng)Foundation框架下的_NSSetObjectValueAndNotify方法予权。那么這個框架內(nèi)部又是怎么實(shí)現(xiàn)的呢,我們可以下斷點(diǎn)转绷,查看下函數(shù)調(diào)用棧:

首先通過設(shè)置一個觀察點(diǎn)伟件,觀察屬性的變化:


image

繼續(xù)執(zhí)行,可以看到函數(shù)調(diào)用棧如下:


image

在結(jié)果發(fā)生改變的地方繼續(xù)下斷點(diǎn)調(diào)試:


image
image

由以上函數(shù)調(diào)用棧议经,我們大致可以猜測出斧账,_NSSetObjectValueAndNotify函數(shù)內(nèi)部實(shí)現(xiàn)過程如下:

1. `-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]:
2. -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:]:
3. [ZJPerson setName:];
4. `NSKeyValueDidChange:
5. `NSKeyValueNotifyObserver:
6. - (void)observeValueForKeyPath:ofObject:change:context

簡化成OC的偽代碼大致如下:

- (void)setName:(NSString *)name{
    _NSSetObjectValueAndNotify();
}

void _NSSetObjectValueAndNotify {
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

- (void)didChangeValueForKey:(NSString *)key{
    [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

NSKVONotifying_ZJPerson內(nèi)部都重寫了哪些方法

可以利用runtime方法打印一下方法列表:

unsigned int count;
Method *methods = class_copyMethodList(object_getClass(self.person), &count);
    
for (NSInteger index = 0; index < count; index++) {
   Method method = methods[index];
   
   NSString *methodStr = NSStringFromSelector(method_getName(method));
   
   NSLog(@"%@\n", methodStr);
}

打印結(jié)果:

2018-05-20 08:57:07.883400+0800 KVO[35888:3218908] setName:
2018-05-20 08:57:07.883571+0800 KVO[35888:3218908] class
2018-05-20 08:57:07.883676+0800 KVO[35888:3218908] dealloc
2018-05-20 08:57:07.883793+0800 KVO[35888:3218908] _isKVOA

簡單分析下重寫這些方法的作用:

class:重寫這個方法谴返,是為了偽裝蘋果自動為我們生成的中間類。
dealloc:應(yīng)該是處理對象銷毀之前的一些收尾工作
_isKVOA:告訴系統(tǒng)使用了kvo

拓展

學(xué)任何東西咧织,通過我們的思考一定會問出一些別的問題嗓袱,通過深入了解kvo,下面兩個問題习绢,是面試經(jīng)常會被問到的渠抹,也是我所能想到的:

  • 如何動態(tài)生成一個類?闪萄?
  • 知道了原理梧却,能不能自己寫一個KVO?败去?

動態(tài)生成一個自己的類

既然是動態(tài)生成放航,肯定是利用了蘋果的runtime機(jī)制,通過上面對KVO的學(xué)習(xí)圆裕,也了解到了runtime的強(qiáng)大之處广鳍。

  • 創(chuàng)建類
Class customClass = objc_allocateClassPair([NSObject class], "ZJCustomClass", 0);
  • 添加實(shí)例變量
// 添加實(shí)例變量
    class_addIvar(customClass, "age", sizeof(int), 0, "i");
// 添加方法
    class_addMethod(customClass, @selector(hahahha), (IMP)hahahha, "V@:");

需要實(shí)現(xiàn)方法:

void hahahha(id self, SEL _cmd)
{
    NSLog(@"hahahha====");
}

- (void)hahahha{

}
  • 注冊到運(yùn)行時(shí)環(huán)境
objc_registerClassPair(customClass);
打印方法列表和成員變量列表吓妆,查看是否創(chuàng)建成功
#pragma mark - Util

- (NSString *)copyMethodsByClass:(Class)cls{
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);
    
    NSString *methodStrs = @"";
    
    for (NSInteger index = 0; index < count; index++) {
        Method method = methods[index];
        
        NSString *methodStr = NSStringFromSelector(method_getName(method));
        
        methodStrs = [NSString stringWithFormat:@"%@ ", methodStr];
    }
    
    free(methods);
    
    return methodStrs;
}

- (NSString *)copyIvarsByClass:(Class)cls{
    unsigned int count;
    Ivar *ivars = class_copyIvarList(cls, &count);
    
    NSMutableString *ivarStrs = [NSMutableString string];
    
    for (NSInteger index = 0; index < count; index++) {
        Ivar ivar = ivars[index];
        
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];  //獲取成員變量的名字
        
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; //獲取成員變量的數(shù)據(jù)類型
        
        [ivarStrs appendString:@"\n"];
        [ivarStrs appendString:ivarName];
        [ivarStrs appendString:@"-"];
        [ivarStrs appendString:ivarType];
        
    }
    
    free(ivars);
    
    return ivarStrs;
}

調(diào)用方法可看到創(chuàng)建成功:

NSLog(@"%@", [self copyMethodsByClass:customClass]);
NSLog(@"%@", [self copyIvarsByClass:customClass]);
image

動態(tài)創(chuàng)建類大致就這些步驟赊时。。行拢。

自己動手寫一個KVO

KVO底層實(shí)現(xiàn)還是很復(fù)雜的祖秒,下面我只是簡單的寫下實(shí)現(xiàn)過程:

  • 因?yàn)樗且粋€非正式協(xié)議,給NSObject新建一個Category剂陡,NSObject+kvo.h,添加監(jiān)聽方法:

.h文件

#import <Foundation/Foundation.h>

@interface NSObject (kvo)

- (void)zj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

@end

.m文件

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

@implementation NSObject (kvo)

- (void)zj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
    //動態(tài)添加一個類
    NSString *originClassName = NSStringFromClass([self class]);
    
    NSString *newClassName = [@"ZJKVO_" stringByAppendingString:originClassName];
    
    const char *newName = [newClassName UTF8String];
    
    // 繼承自當(dāng)前類狈涮,創(chuàng)建一個子類
    Class kvoClass = objc_allocateClassPair([self class], newName, 0);
    
    // 添加setter方法
    class_addMethod(kvoClass, @selector(setName:), (IMP)setName, "v@:@");
    
    //注冊新添加的這個類
    objc_registerClassPair(kvoClass);
    
    // 修改isa指針,由ZJPerson指向ZJKVO_Person
    object_setClass(self, kvoClass);
    
    // 保存觀察者屬性到當(dāng)前類中
    objc_setAssociatedObject(self, (__bridge const void *)@"observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

#pragma mark - 重寫父類方法

void setName(id self, SEL _cmd, NSString *name) {
    
    // 保存當(dāng)前KVO的類
    Class kvoClass = [self class];
    
    // 將self的isa指針指向父類ZJPerson鸭栖,調(diào)用父類setter方法
    object_setClass(self, class_getSuperclass([self class]));
    
    // 調(diào)用父類setter方法歌馍,重新復(fù)制
    objc_msgSend(self, @selector(setName:), name);
    
    // 取出ZJKVO_Person觀察者
    id objc = objc_getAssociatedObject(self, (__bridge const void *)@"observer");

    // 通知觀察者,執(zhí)行通知方法
    objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:), name, self, nil, name);
    
    // 重新修改為ZJKVO_Person類
    object_setClass(self, kvoClass);
}

注意一
要修改下xcode中的一個配置晕鹊,將它改為NO松却,否則會報(bào)參數(shù)太多的錯誤:

image

注意二

解釋下代碼中v@:@的意思:

  • 第一個v表示方法返回值void
  • 第二三個@:一般是一塊的,因?yàn)楹瘮?shù)至少有兩個參數(shù)self和_cmd溅话,一般是固定寫法
  • 最后一個@表示參數(shù)類型晓锻,是一個對象

下面在代碼中實(shí)驗(yàn),看下我們自己寫的kvo有沒有執(zhí)行:

修改添加監(jiān)聽者的方法飞几,改成我們自己的

[self.person zj_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

看下回調(diào)中的打友舛摺:


image

發(fā)現(xiàn)確實(shí)監(jiān)聽到了。屑墨。躁锁。

代碼地址

總結(jié)

kvo用法其實(shí)非常簡單纷铣,但是深入了解,深入思考的話战转,知識點(diǎn)非常多搜立。花了一天多的時(shí)間槐秧,期間查閱了很多文檔(發(fā)現(xiàn)官方文檔真的是非常有用)啄踊,總算是寫完了,對KVO有了一個更深入的認(rèn)識和理解刁标。今天是520颠通,感謝女朋友的理解,終于可以陪她出去玩了命雀,哈哈蒜哀。。吏砂。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市乘客,隨后出現(xiàn)的幾起案子狐血,更是在濱河造成了極大的恐慌,老刑警劉巖易核,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匈织,死亡現(xiàn)場離奇詭異,居然都是意外死亡呀枢,警方通過查閱死者的電腦和手機(jī)菠发,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門泪姨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人乡小,你說我怎么就攤上這事《罚” “怎么了满钟?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胳喷。 經(jīng)常有香客問我湃番,道長,這世上最難降的妖魔是什么吭露? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任吠撮,我火速辦了婚禮,結(jié)果婚禮上讲竿,老公的妹妹穿的比我還像新娘泥兰。我一直安慰自己择浊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布逾条。 她就那樣靜靜地躺著琢岩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪师脂。 梳的紋絲不亂的頭發(fā)上担孔,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天,我揣著相機(jī)與錄音吃警,去河邊找鬼糕篇。 笑死,一個胖子當(dāng)著我的面吹牛酌心,可吹牛的內(nèi)容都是我干的拌消。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼安券,長吁一口氣:“原來是場噩夢啊……” “哼墩崩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起侯勉,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤鹦筹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后址貌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铐拐,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年练对,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了遍蟋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡螟凭,死狀恐怖虚青,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赂摆,我是刑警寧澤挟憔,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站烟号,受9級特大地震影響绊谭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜汪拥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一达传、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦宪赶、人聲如沸宗弯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蒙保。三九已至,卻和暖如春欲主,著一層夾襖步出監(jiān)牢的瞬間邓厕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工扁瓢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留详恼,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓引几,卻偏偏與公主長得像昧互,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子伟桅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

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

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,373評論 8 265
  • 上半年有段時(shí)間做了一個項(xiàng)目敞掘,項(xiàng)目中聊天界面用到了音頻播放,涉及到進(jìn)度條贿讹,當(dāng)時(shí)做android時(shí)候處理的不太好渐逃,由于...
    DaZenD閱讀 3,017評論 0 26
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,709評論 0 9
  • OC語言基礎(chǔ) 1.類與對象 類方法 OC的類方法只有2種:靜態(tài)方法和實(shí)例方法兩種 在OC中民褂,只要方法聲明在@int...
    奇異果好補(bǔ)閱讀 4,271評論 0 11
  • DHCP 中繼:用于DHCP server與client不同一個網(wǎng)段時(shí),協(xié)助Server與CLient進(jìn)行 DHC...
    狗達(dá)Da閱讀 175評論 0 0