iOS開發(fā)中KVO的內(nèi)部實(shí)現(xiàn)
09年的一篇文章,比較深入地闡述了KVO的內(nèi)部實(shí)現(xiàn)。
KVO是實(shí)現(xiàn)Cocoa Bindings的基礎(chǔ),它提供了一種方法,當(dāng)某個(gè)屬性改變時(shí),相應(yīng)的objects會(huì)被通知到。在其他語言中,這種觀察者模式通常需要單獨(dú)實(shí)現(xiàn),而在Objective-C中,通常無須增加額外代碼即可使用。
概覽
這是怎么實(shí)現(xiàn)的呢?其實(shí)這都是通過Objective-C強(qiáng)大的運(yùn)行時(shí)(runtime)實(shí)現(xiàn)的。當(dāng)你***次觀察某個(gè)object 時(shí),runtime會(huì)創(chuàng)建一個(gè)新的繼承原先class的subclass。在這個(gè)新的class中,它重寫了所有被觀察的key,然后將object的isa
指針指向新創(chuàng)建的class(這個(gè)指針告訴Objective-C運(yùn)行時(shí)某個(gè)object到底是哪種類型的object)。所以object神奇地變成了新的子類的實(shí)例。
這些被重寫的方法實(shí)現(xiàn)了如何通知觀察者們。當(dāng)改變一個(gè)key時(shí),會(huì)觸發(fā)setKey
方法,但這個(gè)方法被重寫了,并且在內(nèi)部添加了發(fā)送通知機(jī)制。(當(dāng)然也可以不走setXXX方法,比如直接修改iVar,但不推薦這么做)。
有意思的是:蘋果不希望這個(gè)機(jī)制暴露在外部。除了setters,這個(gè)動(dòng)態(tài)生成的子類同時(shí)也重寫了-class
方法,依舊返回原先的class!如果不仔細(xì)看的話,被KVO過的object看起來和原先的object沒什么兩樣。
深入探究
下面來看看這些是如何實(shí)現(xiàn)的。我寫了個(gè)程序來演示隱藏在KVO背后的機(jī)制。
- // gcc -o kvoexplorer -framework Foundation kvoexplorer.m
- #import <Foundation/Foundation.h>
- #import <objc/runtime.h>
- @interface TestClass : NSObject
- {
- int x;
- int y;
- int z;
- }
- @property int x;
- @property int y;
- @property int z;
- @end
- @implementation TestClass
- @synthesize x, y, z;
- @end
- static NSArray *ClassMethodNames(Class c)
- {
- NSMutableArray *array = [NSMutableArray array];
- unsigned int methodCount = 0;
- Method *methodList = class_copyMethodList(c, &methodCount);
- unsigned int i;
- for(i = 0; i < methodCount; i++)
- [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
- free(methodList);
- return array;
- }
- static void PrintDescription(NSString *name, id obj)
- {
- NSString *str = [NSString stringWithFormat:
- @"%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
- name,
- obj,
- class_getName([obj class]),
- class_getName(obj->isa),
- [ClassMethodNames(obj->isa) componentsJoinedByString:@", "]];
- printf("%s\n", [str UTF8String]);
- }
- int main(int argc, char **argv)
- {
- [NSAutoreleasePool new];
- TestClass *x = [[TestClass alloc] init];
- TestClass *y = [[TestClass alloc] init];
- TestClass *xy = [[TestClass alloc] init];
- TestClass *control = [[TestClass alloc] init];
- [x addObserver:x forKeyPath:@"x" options:0 context:NULL];
- [xy addObserver:xy forKeyPath:@"x" options:0 context:NULL];
- [y addObserver:y forKeyPath:@"y" options:0 context:NULL];
- [xy addObserver:xy forKeyPath:@"y" options:0 context:NULL];
- PrintDescription(@"control", control);
- PrintDescription(@"x", x);
- PrintDescription(@"y", y);
- PrintDescription(@"xy", xy);
- printf("Using NSObject methods, normal setX: is %p, overridden setX: is %p\n",
- [control methodForSelector:@selector(setX:)],
- [x methodForSelector:@selector(setX:)]);
- printf("Using libobjc functions, normal setX: is %p, overridden setX: is %p\n",
- method_getImplementation(class_getInstanceMethod(object_getClass(control),
- @selector(setX:))),
- method_getImplementation(class_getInstanceMethod(object_getClass(x),
- @selector(setX:))));
- return 0;
- }
我們從頭到尾細(xì)細(xì)看來。
首先定義了一個(gè)TestClass
的類,它有3個(gè)屬性。
然后定義了一些方便調(diào)試的方法。ClassMethodNames
使用Objective-C運(yùn)行時(shí)方法來遍歷一個(gè)class,得到方法列表。注意,這些方法不包括父類的方法。PrintDescription
打印object的所有信息,包括class信息(包括-class
和通過運(yùn)行時(shí)得到的class),以及這個(gè)class實(shí)現(xiàn)的方法。
然后創(chuàng)建了4個(gè)TestClass
實(shí)例,每一個(gè)都使用了不同的觀察方式。x
實(shí)例有一個(gè)觀察者觀察x
key,y
, xy
也類似。為了做比較,z
key沒有觀察者。***control
實(shí)例沒有任何觀察者。
然后打印出4個(gè)objects的description。
之后繼續(xù)打印被重寫的setter內(nèi)存地址,以及未被重寫的setter的內(nèi)存地址做比較。這里做了兩次,是因?yàn)?code>-methodForSelector:沒能得到重寫的方法。KVO試圖掩蓋它實(shí)際上創(chuàng)建了一個(gè)新的subclass這個(gè)事實(shí)!但是使用運(yùn)行時(shí)的方法就原形畢露了。
運(yùn)行代碼
看看這段代碼的輸出
- control: <TestClass: 0x104b20>
- NSObject class TestClass
- libobjc class TestClass
- implements methods <setX:, x, setY:, y, setZ:, z>
- x: <TestClass: 0x103280>
- NSObject class TestClass
- libobjc class NSKVONotifying_TestClass
- implements methods <setY:, setX:, class, dealloc, _isKVOA>
- y: <TestClass: 0x104b00>
- NSObject class TestClass
- libobjc class NSKVONotifying_TestClass
- implements methods <setY:, setX:, class, dealloc, _isKVOA>
- xy: <TestClass: 0x104b10>
- NSObject class TestClass
- libobjc class NSKVONotifying_TestClass
- implements methods <setY:, setX:, class, dealloc, _isKVOA>
- Using NSObject methods, normal setX: is 0x195e, overridden setX: is 0x195e
- Using libobjc functions, normal setX: is 0x195e, overridden setX: is 0x96a1a550
首先,它輸出了control
object,沒有任何問題,它的class是TestClass
,并且實(shí)現(xiàn)了6個(gè)set/get方法。
然后是3個(gè)被觀察的objects。注意-class
仍然顯示的是TestClass
,使用object_getClass
顯示了這個(gè)object的真面目:它是NSKVONotifying_TestClass
的一個(gè)實(shí)例。這個(gè)NSKVONotifying_TestClass
就是動(dòng)態(tài)生成的subclass!
注意,它是如何實(shí)現(xiàn)這兩個(gè)被觀察的setters的。你會(huì)發(fā)現(xiàn),它很聰明,沒有重寫-setZ:
,雖然它也是個(gè) setter,因?yàn)樗鼪]有被觀察。同時(shí)注意到,3個(gè)實(shí)例對(duì)應(yīng)的是同一個(gè)class,也就是說兩個(gè)setters都被重寫了,盡管其中的兩個(gè)實(shí)例只觀察了一 個(gè)屬性。這會(huì)帶來一點(diǎn)效率上的問題,因?yàn)榧词箾]有被觀察的property也會(huì)走被重寫的setter,但蘋果顯然覺得這比分開生成動(dòng)態(tài)的 subclass更好,我也覺得這是個(gè)正確的選擇。
你會(huì)看到3個(gè)其他的方法。有之前提到過的被重寫的-class
方法,假裝自己還是原來的class。還有-dealloc
方法處理一些收尾工作。還有一個(gè)_isKVOA
方法,看起來像是一個(gè)私有方法。
接下來,我們輸出-setX:
的實(shí)現(xiàn)。使用-methodForSelector:
返回的是相同的值。因?yàn)?code>-setX:已經(jīng)在子類被重寫了,這也就意味著methodForSelector:
在內(nèi)部實(shí)現(xiàn)中使用了-class
,于是得到了錯(cuò)誤的結(jié)果。
***我們通過運(yùn)行時(shí)得到了不同的輸出結(jié)果。
作為一個(gè)優(yōu)秀的探索者,我們進(jìn)入debugger來看看這第二個(gè)方法的實(shí)現(xiàn)到底是怎樣的:
- (gdb) print (IMP)0x96a1a550
- $1 = (IMP) 0x96a1a550 <_NSSetIntValueAndNotify>
看起來是一個(gè)內(nèi)部方法,對(duì)Foundation
使用nm -a
得到一個(gè)完整的私有方法列表:
- 0013df80 t __NSSetBoolValueAndNotify
- 000a0480 t __NSSetCharValueAndNotify
- 0013e120 t __NSSetDoubleValueAndNotify
- 0013e1f0 t __NSSetFloatValueAndNotify
- 000e3550 t __NSSetIntValueAndNotify
- 0013e390 t __NSSetLongLongValueAndNotify
- 0013e2c0 t __NSSetLongValueAndNotify
- 00089df0 t __NSSetObjectValueAndNotify
- 0013e6f0 t __NSSetPointValueAndNotify
- 0013e7d0 t __NSSetRangeValueAndNotify
- 0013e8b0 t __NSSetRectValueAndNotify
- 0013e550 t __NSSetShortValueAndNotify
- 0008ab20 t __NSSetSizeValueAndNotify
- 0013e050 t __NSSetUnsignedCharValueAndNotify
- 0009fcd0 t __NSSetUnsignedIntValueAndNotify
- 0013e470 t __NSSetUnsignedLongLongValueAndNotify
- 0009fc00 t __NSSetUnsignedLongValueAndNotify
- 0013e620 t __NSSetUnsignedShortValueAndNotify
這個(gè)列表也能發(fā)現(xiàn)一些有趣的東西。比如蘋果為每一種primitive type都寫了對(duì)應(yīng)的實(shí)現(xiàn)。Objective-C的object會(huì)用到的其實(shí)只有__NSSetObjectValueAndNotify
,但需要一整套來對(duì)應(yīng)剩下的,而且看起來也沒有實(shí)現(xiàn)完全,比如long dobule
或_Bool
都沒有。甚至沒有為通用指針類型(generic pointer type)提供方法。所以,不在這個(gè)方法列表里的屬性其實(shí)是不支持KVO的。
KVO是一個(gè)很強(qiáng)大的工具,有時(shí)候過于強(qiáng)大了,尤其是有了自動(dòng)觸發(fā)通知機(jī)制?,F(xiàn)在你知道它內(nèi)部是怎么實(shí)現(xiàn)的了,這些知識(shí)或許能幫助你更好地使用它,或在它出錯(cuò)時(shí)更方便調(diào)試。
如果你打算使用KVO,或許可以看一下我的另一篇文章Key-Value Observing Done Right
【移動(dòng)開發(fā)視頻課程推薦】
- iOS培訓(xùn)之Objective-C基礎(chǔ)視頻教程(40集)
- Cocos2d-x從零開始【5天掌握跨平臺(tái)游戲開發(fā)利器】(12集)
- Objective C編程基礎(chǔ)(24集)
- Android技術(shù)輕松入門課程(12集)
- 微信開放平臺(tái)-Android應(yīng)用接入(4集)
- Cocos2d-x跨平臺(tái)游戲開發(fā)入門基礎(chǔ)(29集)
- iOS開發(fā)視頻教程-iOS網(wǎng)絡(luò)編程【高級(jí)篇】(39集)
- 移動(dòng)應(yīng)用用戶體驗(yàn)設(shè)計(jì)高級(jí)課程(60集)
- 從零學(xué)習(xí)iOS開發(fā)–UI多視圖(30集)
- iOS開發(fā)視頻教程【基礎(chǔ)入門篇】