KVO的內部實現
KVO是實現Cocoa Bindings的基礎,它提供了一種方法,當某個屬性改變時,相應的objects會被通知到。在其他語言中,這種觀察者模式通常需要單獨實現,而在Objective-C中,通常無須增加額外程式碼即可使用。
概覽
這是怎麼實現的呢?其實這都是通過Objective-C強大的執行時(runtime)實現的。當你第一次觀察某個object 時,runtime會建立一個新的繼承原先class的subclass。在這個新的class中,它重寫了所有被觀察的key,然後將object的isa指標指向新建立的class(這個指標告訴Objective-C執行時某個object到底是哪種型別的object)。所以object神奇地變成了新的子類的例項。
這些被重寫的方法實現瞭如何通知觀察者們。當改變一個key時,會觸發setKey方法,但這個方法被重寫了,並且在內部新增了傳送通知機制。(當然也可以不走setXXX方法,比如直接修改iVar,但不推薦這麼做)。
有意思的是:蘋果不希望這個機制暴露在外部。除了setters,這個動態生成的子類同時也重寫了-class方法,依舊返回原先的class!如果不仔細看的話,被KVO過的object看起來和原先的object沒什麼兩樣。
深入探究
下面來看看這些是如何實現的。我寫了個程式來演示隱藏在KVO背後的機制。
// 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;
}
我們從頭到尾細細看來。
首先定義了一個TestClass的類,它有3個屬性。
然後定義了一些方便除錯的方法。ClassMethodNames使用Objective-C執行時方法來遍歷一個class,得到方法列表。注意,這些方法不包括父類的方法。PrintDescription列印object的所有資訊,包括class資訊(包括-class和通過執行時得到的class),以及這個class實現的方法。
然後建立了4個TestClass例項,每一個都使用了不同的觀察方式。x例項有一個觀察者觀察xkey,y, xy也類似。為了做比較,zkey沒有觀察者。最後control例項沒有任何觀察者。
然後列印出4個objects的description。
之後繼續列印被重寫的setter記憶體地址,以及未被重寫的setter的記憶體地址做比較。這裡做了兩次,是因為-methodForSelector:沒能得到重寫的方法。KVO試圖掩蓋它實際上建立了一個新的subclass這個事實!但是使用執行時的方法就原形畢露了。
執行程式碼
看看這段程式碼的輸出
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
首先,它輸出了controlobject,沒有任何問題,它的class是TestClass,並且實現了6個set/get方法。
然後是3個被觀察的objects。注意-class仍然顯示的是TestClass,使用object_getClass顯示了這個object的真面目:它是NSKVONotifying_TestClass的一個例項。這個NSKVONotifying_TestClass就是動態生成的subclass!
注意,它是如何實現這兩個被觀察的setters的。你會發現,它很聰明,沒有重寫-setZ:,雖然它也是個 setter,因為它沒有被觀察。同時注意到,3個例項對應的是同一個class,也就是說兩個setters都被重寫了,儘管其中的兩個例項只觀察了一 個屬性。這會帶來一點效率上的問題,因為即使沒有被觀察的property也會走被重寫的setter,但蘋果顯然覺得這比分開生成動態的 subclass更好,我也覺得這是個正確的選擇。
你會看到3個其他的方法。有之前提到過的被重寫的-class方法,假裝自己還是原來的class。還有-dealloc方法處理一些收尾工作。還有一個_isKVOA方法,看起來像是一個私有方法。
接下來,我們輸出-setX:的實現。使用-methodForSelector:返回的是相同的值。因為-setX:已經在子類被重寫了,這也就意味著methodForSelector:在內部實現中使用了-class,於是得到了錯誤的結果。
最後我們通過執行時得到了不同的輸出結果。
作為一個優秀的探索者,我們進入debugger來看看這第二個方法的實現到底是怎樣的:
(gdb) print (IMP)0x96a1a550
$1 = (IMP) 0x96a1a550 <_NSSetIntValueAndNotify>
看起來是一個內部方法,對Foundation使用nm -a得到一個完整的私有方法列表:
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
這個列表也能發現一些有趣的東西。比如蘋果為每一種primitive type都寫了對應的實現。Objective-C的object會用到的其實只有__NSSetObjectValueAndNotify,但需要一整套來對應剩下的,而且看起來也沒有實現完全,比如long dobule或_Bool都沒有。甚至沒有為通用指標型別(generic pointer type)提供方法。所以,不在這個方法列表裡的屬性其實是不支援KVO的。
KVO是一個很強大的工具,有時候過於強大了,尤其是有了自動觸發通知機制。現在你知道它內部是怎麼實現的了,這些知識或許能幫助你更好地使用它,或在它出錯時更方便除錯。
相關文章
- (iOS)KVO 的實現原理iOS
- mysqldump的內部實現原理MySql
- gostring的內部實現Go
- 使用 Block 實現 KVOBloC
- 手動實現KVO
- HashMap的內部實現機制HashMap
- KVO使用及實現原理
- KVC、KVO實現過程
- iOS 揭露Block的內部實現原理iOSBloC
- 帶你看懂Dictionary的內部實現
- 深入 Python 字典的內部實現Python
- 深入 Python 列表的內部實現Python
- SDWebImage內部實現過程Web
- KVO的使用和底層實現原理
- 使用Runtime來實現自己的KVO
- 精讀《JS 陣列的內部實現》JS陣列
- kafka的內部實現、安裝和使用Kafka
- Object.create(..)和new(..)的內部實現Object
- C#中Dictionary的內部實現剖析C#
- ORACLE NUMBER型別內部實現Oracle型別
- 解讀Promise內部實現原理Promise
- MG--探究KVO的底層實現原理
- STL vector的內部實現原理及基本用法
- 浮點數演算法的內部實現演算法
- 【譯】Go 切片:用法和內部實現Go
- 與你探索classnames模組內部實現
- 面試題 SDWebImage內部實現過程面試題Web
- 用程式碼探討KVC/KVO的實現原理
- 用程式碼探討 KVC/KVO 的實現原理
- iOS–KVO的實現原理與具體應用iOS
- Java 阻塞佇列(BlockingQueue)的內部實現原理Java佇列BloC
- 匿名內部類方式實現執行緒的建立執行緒
- Python for迴圈內部實現的一個samplePython
- 變數在 PHP7 內部的實現(一)變數PHP
- 變數在 PHP7 內部的實現(二)變數PHP
- css如何實現只保留內部邊框CSS
- ICE框架元件內部實現與特點框架元件
- iOS窺探KVO底層實現實戰篇iOS