iOS觀察模式之KVO實現探究

本篇會對KVO的實現進行探究,不涉及太多KVO的使用方法,但是會有一些使用時的思考。

一、使用上的疑問

1.keyPath是什么

當我們使用@property時候,keyPath是指的是我們的屬性名,實例變量或者是存取方法?

:point_down: 對一個屬性值使用@synthesize重新定義了存儲變量

```

# import"Person.h"

@interface Student:Person

@property(nonatomic,strong)NSString* mark;

@end

@implementation ?Student

@synthesizemark = abc;

- (void)setMark:(NSString*)newMark {

?abc = newMark;

}

- (NSString*)mark {

returnabc;

}

main() {?

Student *stu = [[Student alloc] init];?

stu.mark =@"65";

?StudentKvoObserver *stuObserver = [[StudentKvoObserver alloc] initWithStudent:stu]; [stuObserver addObserverForKeyPath:@"mark"];

```

// 重命名get方法stu.mark =@"85";}

實際結果是,能夠監聽到mark值的變化,反之,我將mark替換成真正的實例變量abc時,無法獲取狀態。

現在想想其實答案早就存在了,我們不做顯示的@synthesize的指定時,其實等價于 @synthesize mark = _mark; ,由此看來keyPath實際指的并不是真正存儲你數據的變量。

2.KVO是否能夠繼承

我是否能夠監聽我父類里的屬性,哪怕他并沒有暴露出來?通過某些手段得(猜)到了keyPath,然后去監聽它甚至是KVC修改他的值。

子類繼承父類的一個屬性,當這個屬性被改變時,KVO能否觀察到?

子類繼承父類的一個未暴露的屬性,當這個屬性被改變時,KVO能否觀察到?

子類繼承父類屬性并重寫了它的setter方法,當這個屬性被改變時,KVO能否觀察到?

// Person類@interfacePerson:NSObject

@property(nonatomic,strong)NSString*firstName;

@property(nonatomic,strong)NSString*lastName;

@property(nonatomic,strong,readonly)NSString*fullName;

- (void)setNewInnerName:(NSString*)str;

@end

@interfacePerson()

@property(nonatomic,strong)NSString*innerName;

@end@implementationPerson

- (void)setNewInnerName:(NSString*)str {

self.innerName = str;// 通過get、set訪問 觸發KVO

// [self setValue:str forKey:@"innerName"];

// KVC方式,其實調用的也是setter方法 觸發KVO

// _innerName = str;

// 直接訪問成員變量,不觸發KVO}

// Student類

@interface ?Student:Person

@end

@implementationStudent

- (void)setFirstName:(NSString*)firstName {

NSLog(@"重寫的setFirstName方法");

}

@end

// 執行文件

main() {

?Person *p = [[Person alloc] init];?

p.firstName =@"zhao";?

p.lastName =@"zhiyu";

?PersonKvoObserver *personKvoObserver = [[PersonKvoObserver alloc]initWithPerson:p];?

[personKvoObserver addObserverForKeyPath:@"fullName"];

// 屬性關聯

[personKvoObserver addObserverForKeyPath:@"innerName"];

// 內部屬性

p.firstName =@"zhao1";

?[p setNewInnerName:@"newInnerNmame"];

// 沒有暴露的屬性的get、set方法被調用時,也會發送通知// 子類的屬性監聽

Student *stu = [[Student alloc] init];

?stu.firstName =@"stu";?

stu.lastName =@"dent";

?StudentKvoObserver *stuObserver = [[StudentKvoObserver alloc] initWithStudent:stu]; [stuObserver addObserverForKeyPath:@"fullName"];// 子類繼承屬性依舊被監聽[stuObserver addObserverForKeyPath:@"firstName"];// 重寫方法,不加super,依舊會監聽kvo[stuObserver addObserverForKeyPath:@"innerName"];??

stu.firstName =@"stu1";

stu.lastName =@"dent1";

?[stu setNewInnerName:@"newInnerNmame"];

// 沒有暴露的屬性的get、set方法被調用時,也會發送通知}

通過上面的例子,我們能看出幾點:

①通過KVO,能觀察父類的屬性值。

②只要知道了keyPath,不管有沒有暴露方法,依舊可以通過KVO方式觀察值的變化,而且同屬性一樣,可以被繼承。

③子類重寫父類的set方法,也并不會影響KVO的觀察。

從這兒開始就有點好奇了,這個KVO是否通過子類化的方法實現?那如何讓子類的繼承屬性也能被監聽到?了解到KVO依賴setter方法的重寫,那我子類重寫的setter方法之后,為什么子類繼承屬性的監聽依然生效?

3.跨線程的監聽

我們知道使用Notification時,跨線程發送通知是無法被接受到的,那么現在看看KVO在多線程中的表現。

// 在兩個線程定義目標和觀察者

dispatch_queue_t ?concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);

// dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

__block Student *stu1 =nil;

dispatch_async(concurrentQueue, ^{// 對象屬性

stu1 = [[Student alloc] init];

NSLog(@"Student %@",[NSDate ?new]);?

stu1.lastName =@"yyyyyyy";?

});

__block StudentKvoObserver *stuObserver1;

dispatch_async(concurrentQueue, ^{?

sleep(2);?

stuObserver1 = [[StudentKvoObserver alloc] initWithStudent:stu1];

?[stuObserver1 addObserverForKeyPath:@"fullName"];

// 子類繼承屬性依舊被監聽

NSLog(@" StudentKvoObserver %@",[NSDate new]); }); dispatch_barrier_async(concurrentQueue, ^{

NSLog(@"dispatch_barrier_async %@",[NSDate new]);

NSLog(@"zzzzzz start%@",[NSDate new]);

?stu1.lastName =@"zzzzzz";

NSLog(@"zzzzzz end%@",[NSDate new]); });

輸出結果

2016-10-11 10:46:53.319 KVCLearn[3364:331572] Student 2016-10-11 02:46:53 +0000

2016-10-11 10:46:55.324 KVCLearn[3364:331578] StudentKvoObserver 2016-10-11 02:46:55 +0000

2016-10-11 10:46:55.325 KVCLearn[3364:331578] dispatch_barrier_async 2016-10-11 02:46:55 +0000

2016-10-11 10:46:55.325 KVCLearn[3364:331578] zzzzzz start2016-10-11 02:46:55 +0000

2016-10-11 10:46:55.326 KVCLearn[3364:331578] fullName

{

kind = 1;

new = “(null)zzzzzz”;

old = “(null)yyyyyyy”;

}

2016-10-11 10:46:55.326 KVCLearn[3364:331578] zzzzzz end2016-10-11 02:46:55 +0000

可以看到在兩個不同的線程里創建的Observer和Target,觀察變化也是能夠生效的。

這里有一個關于GCD的問題,這里我使用了dispatch_barrier_async,分發到自定義的并發隊列上,這時barrier是正常工作的,保證了第三個task在前兩個執行完之后執行。但是當我直接使用系統全局的并發隊列時,barrier不起作用,不能保證他們的執行順序。這里希望有高人看見了能解答下。

二、實現探究

1.API接口

Foundation里關于KVO的部分都定義在NSKeyValueObserving.h中,KVO通過以下三個NSObject分類實現。

NSObject(NSKeyValueObserving)

NSObject(NSKeyValueObserverRegistration)

NSObject(NSKeyValueObservingCustomization)

這里會從NSObject (NSKeyValueObserverRegistration) 的 - addObserver:forKeyPath:options:context: 為入口,去一步步分析如何整個KVO的實現方式。

2.先說結論

實現方式:

一個對象在被調用addObserver方法時,會動態創建一個KVO前綴的原類的子類,用來重寫所有的setter方法,并且該子類的 - (Class) class 和 - (Class) superclass 方法會被重寫,返回父類(原始類)的Class。最后會將當前對象的類改為這個KVO前綴的子類。

比較繞,讓我們來看個例子。比如說類Person的實例person調用了addObserver方法時,addObserver方法內部給你創建了一個KVOPerson類,KVOPerson的所有的setter方法會被重寫,它的class和superClass方法會被改寫成返回Person和NSObject,之后使用 object_setClass 將KVOPerson設置成person的class。

當我們調用person的setName方法時,實際是調用的一個KVOPerson實例的setName方法,但由于重寫了class,在外部看不出來其中的差別。在setter方法中,我們在實際值被改變的前后回調用 - (void)willChangeValueForKey:(NSString *)key; 和 - (void)didChangeValueForKey:(NSString *)key; 方法,通知觀察者值的變化。

3.代碼

源碼是來自GNUSetup里的Foundation,據說和apple的實現類似,只是相關API的版本會比較老一些。我們先從addObserver方法開始。

@implementationNSObject(NSKeyValueObserverRegistration)- (void) addObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath options: (NSKeyValueObservingOptions)options context: (void*)aContext{ ....// 1.使用當前類創建GSKVOReplacement對象r = replacementForClass([selfclass]); .... info = (GSKVOInfo*)[selfobservationInfo];if(info ==nil) { info = [[GSKVOInfo alloc] initWithInstance:self]; [selfsetObservationInfo: info];//2.重新設置classobject_setClass(self, [r replacement]); } ....//3.重寫replace的setter方法[r overrideSetterFor: aPath];//4.注冊當前類和觀察者到全局表中[info addObserver: anObserver forKeyPath: aPath options: options context: aContext];}

忽略了一些分支,可以看到主要為上面四個步驟。我們可以一個一個拆開來看。

replacementForClass

// 單例生成一個GSKVOReplacement對象,保證一個類只有一個KVO子類staticGSKVOReplacement *replacementForClass(Class c){ GSKVOReplacement *r; setup(); [kvoLock lock]; r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);if(r ==nil) { r = [[GSKVOReplacement alloc] initWithClass: c];NSMapInsert(classTable, (void*)c, (void*)r); } [kvoLock unlock];returnr;}- (id) initWithClass: (Class)aClass{NSValue*template;NSString*superName;NSString*name; original = aClass; superName =NSStringFromClass(original); name = [@"GSKVO"stringByAppendingString: superName];// 添加前綴template = GSObjCMakeClass(name, superName,nil);// 通過objc_allocateClassPair得到class指針GSObjCAddClasses([NSArrayarrayWithObject: template]);// objc_registerClassPair注冊classreplacement =NSClassFromString(name);// 前面動態生成且注冊了GSKVO子類,然后就可以通過該方法得到// 添加模板類的一些方法,包括重寫class和superClass讓對象類型不暴露,// setValue:forkey在數據改變前后加上willChange和didChange方法GSObjCAddClassBehavior(replacement, baseClass);/* Create the set of setter methods overridden.

*/keys = [NSMutableSetnew];returnself;}

object_setClass(self, [r replacement]);

// replace就是新生成的KVOXXX的class@interfaceGSKVOReplacement:NSObject{ Class original;/* The original class */Class replacement;/* The replacement class */NSMutableSet*keys;/* The observed setter keys */}replacement =NSClassFromString(name);// 在initWithClass方法中賦值

overrideSetterFor

重寫setter方法,在值改變前后添加上willChange&didChange- (void) overrideSetterFor: (NSString*)aKey{if([keys member: aKey] ==nil) {NSMethodSignature*sig;// 當前key值對應setter的方法簽名SEL sel;// 當前key值對應setter的方法名selectorIMP imp;// 當前key值對應setter的函數指針IMPconstchar*type;NSString*a[2];unsignedi;BOOLfound =NO;// 得到setXxxx:和_setXxxx:方法名a[0] = [NSStringstringWithFormat:@"set%@%@:", tmp, suffix]; a[1] = [NSStringstringWithFormat:@"_set%@%@:", tmp, suffix];for(i =0; i <2; i++) {/*

得到方法簽名

*/sel =NSSelectorFromString(a[i]); sig = [original instanceMethodSignatureForSelector: sel]; type = [sig getArgumentTypeAtIndex:2];// 第三個參數即入參的類型switch(*type) {// 字符case_C_CHR:case_C_UCHR: imp = [[GSKVOSetter class] instanceMethodForSelector:@selector(setterChar:)];// 返回setterChar:函數的函數指針IMPbreak;// 對象、類、指針case_C_ID:case_C_CLASS:case_C_PTR: imp = [[GSKVOSetter class] instanceMethodForSelector:@selector(setter:)];// 返回setter:函數的函數指針IMP,后面有詳解break;break; ....default: imp =0;break; }if(imp !=0) {if(class_addMethod(replacement, sel, imp, [sig methodType]))// 將原sel和新imp加到replacement類中去{ found =YES; }else{NSLog(@"Failed to add setter method for %s to %s", sel_getName(sel), class_getName(original)); } } }if(found ==YES) { [keys addObject: aKey]; } }}

這個步驟是將keypath對應的setter方法重寫找出來,把原有的SEL函數名和重寫后的實現IMP加入到子類中去。這樣做,新生成的子類就有和原父類一樣表現了,再加上之前的class替換,在KVO的對外接口上已經沒有差別。這里也解釋了我一開始的問題,keypath到底指的是什么,其實是setter方法,或者說方法名的后綴。因為我們用@property生成了默認的set方法是滿足規范的,所以會將keypath和property關聯起來。

// setter方法的實現細節@implementationGSKVOSetter- (void) setter: (void*)val{NSString*key; Class c = [selfclass];void(*imp)(id,SEL,void*); imp = (void(*)(id,SEL,void*))[c instanceMethodForSelector: _cmd]; key = newKey(_cmd);if([c automaticallyNotifiesObserversForKey: key] ==YES) {// pre setting code here[selfwillChangeValueForKey: key]; (*imp)(self, _cmd, val);// post setting code here[selfdidChangeValueForKey: key]; }else{ (*imp)(self, _cmd, val); } RELEASE(key);}

對于這個setter方法的實現,我其實是沒大看懂的。 [c instanceMethodForSelector: _cmd]; 這個取到的imp,應該是當前方法的函數指針(GSKVOSetter的setter),后面也是直接調用的該imp實現。沒有找到這個setter是如何和原類方法中實際的setter聯系起來的,之前通過sig方法簽名也只取出了sel,原有實現并沒有出現。希望有大牛看到這個能給我解答一下。

-(void) addObserver: forKeyPath: options: context:

這個部分就是觀察者的注冊了。通過以下類圖可以很方便得看到,所有的類的KVO觀察都是通過infoTable管理的。以被觀察對象實例作key,GSKVOInfo對象為value的形式保存在infoTable表里,每個被觀察者實例會對應多個keypath,每個keypath會對應多個observer對象。順帶提一下,關于Notification的實現也類似,也是全局表維護通知的注冊監聽者和通知名。

GSKVOInfo的結構可以看出來,一個keyPath可以對應有多個觀察者。其中觀察對象的實例和option打包成GSKVOObservation對象保存在一起。

三、總結

看完了KVO的實現部分,我們再回過頭來看開頭提到的幾個問題。

keyPath是什么

首先keyPath,是對于setter方法的關聯,會使用keypath作為后綴去尋找原類的setter方法的方法簽名,和實際存取對象和property名稱沒有關系。所以這也是為什么我們重命名了setter方法之后,沒有辦法再去使用KVO或KVC了,需要手動調用一次willChangeValue方法。

子類繼承父類的一個屬性,當這個屬性被改變時,KVO能否觀察到?

因為繼承的關系Father <- Son <- KVOSon,當我監聽一個父類屬性的keyPath的時候,Son實例同樣可以通過消息查找找到父類的setter方法,再將該方法加入到KVOSon類當中去。

子類繼承父類的一個未暴露的屬性,當這個屬性被改變時,KVO能否觀察到?

由于在overrideSetterFor中,我們是直接通過sel去得到方法簽名signature,所以和暴不暴露沒啥關系。

子類繼承父類屬性并重寫了它的setter方法,當這個屬性被改變時,KVO能否觀察到?

在上一條中知道,其實子類監聽父類屬性,并不依賴繼承,而是通過ISA指針在消息轉發的時候能夠獲取到父類方法就足夠。所以當我們重寫父類setter方法,相當于在子類定義了該setter函數,在我們去用sel找方法簽名時,直接在子類中就拿到了,甚至都不需要去到父類里。所以理解了KVO監聽父類屬性和繼承沒有直接聯系這一點,就不再糾結set方法是否重寫這個問題了。

最后線程安全的部分,沒有做深入的研究,在這篇就不多做表述了。在我貼的源碼中都去掉了很多枝葉,其中就包括加鎖的部分。有興趣的朋友可以去下面貼的源碼地址去看完整版,其中對線程安全的考慮,遞歸鎖、惰性遞歸鎖使用,也是很值得學習的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,755評論 0 9
  • 本篇會對KVO的實現進行探究,不涉及太多KVO的使用方法,但是會有一些使用時的思考。 一、使用上的疑問 1.key...
    孢子菌閱讀 2,195評論 7 13
  • 1、category和extension的區別 category是分類,可以為類增加自定義方法 extension...
    大猿媛閱讀 377評論 1 1
  • “來,咱爺倆干一杯。” 記憶里,除父親之外,不曾有人對我以“咱爺倆”相稱,更何況,父親離開我已十年了。這是十年來我...
    海的波文閱讀 488評論 10 24
  • 2017.07.29 星期六 多云 突然的炎熱讓我有一絲絲的不適應。 這雨?大概下了一個月了吧,纏纏綿綿,絲絲縷...
    若璃殤閱讀 266評論 3 2