本文為大地瓜原創(chuàng)抑月,歡迎知識共享违崇,轉(zhuǎn)載請注明出處。
雖然你不注明出處我也沒什么精力和你計較靴跛。
作者微信號:christgreenlaw
本文的原文缀雳。本文只對其進行翻譯。
What Is It? 這是個啥玩意梢睛?
很多讀者早就知道這個東西了肥印,但是我們還是要簡單的回憶一下:KVO是Cocoa Bindings底層的技術(shù)识椰,讓某些對象能夠在其他對象的屬性被更改時獲得通知。一個對象觀察(observe)另一個對象的key深碱。當被觀察的對象更改了這個key 的值時腹鹉,觀察者就得到了通知。挺好理解的是吧莹痢?牛逼的地方在于:KVO一般不需要給被觀察者寫任何的代碼种蘸。
OverView 概覽
所以說到底這是怎么做到的?竟然不需要給被觀察者寫代碼竞膳?其實秘密就在OC runtime里航瞭。當你第一次觀察一個特定類的一個對象時,KVO就在運行時創(chuàng)建一個全新的類坦辟,這個類其實繼承了你所觀察的類刊侯。在這個新類中,它會將你所觀察的屬性(key)的setter重寫锉走。然后它修改你這個對象的isa指針(也就是告知OC runtime某一塊內(nèi)存到底是什么類型的對象的指針)滨彻,這樣你的對象就成了這個新類的實例。
被重寫的方法執(zhí)行了通知觀察者的工作挪蹭。所以這個邏輯就是:對一個key 的修改必須經(jīng)過key 的set方法亭饵。對set方法的修改使得set方法可以截獲修改,并在set方法被調(diào)用時向觀察者發(fā)送通知梁厉。(當然辜羊,如果你直接修改實例變量的話,你也可能不經(jīng)過set方法就進行修改词顾。KVO要求進行KVO的類要么決不能這樣做(也就是決不能直接修改實例變量)八秃,要么必須將對ivar的直接訪問包裝在手動的通知調(diào)用中。)
有點棘手的是:Apple實際上不希望別人知道這個技術(shù)原理肉盹。不僅僅是setter昔驱,動態(tài)繼承(dynamic subclass)也重寫了-class方法來欺騙你,返回原始的類上忍!如果你不仔細觀察的話骤肛,KVO觀察的對象就和其他沒有被觀察的部分一模一樣。
Digging Deeper 再挖掘一下
不多廢話了窍蓝,我們來看看到底是怎樣工作的萌衬。我寫了一個程序,它展示了KVO背后的原理它抱。由于動態(tài)KVO繼承(dynamic KVO subclass)希望隱藏自己的存在,我主要使用了OC runtime'方法來獲得我們希望獲得的信息朴艰。
以下是代碼:
// 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;
}
大地瓜注:這段代碼似乎在Xcode9下無法正確執(zhí)行观蓄,會提示obj->isa這一行有問題混移。
我們來重頭到尾分析一下。
首先我們定義了一個TestClass
類侮穿,有三個屬性歌径。(KVO對非@property的key也有效,但是這樣最容易定義setter和getter)亲茅。
接下來定義了一組功能函數(shù)回铛。ClassMethodNames
使用了OC運行時方法遍歷一個類然后得到其實現(xiàn)的所有方法的列表。要注意:它只是獲得了在class中直接實現(xiàn)的方法克锣,并不獲取超類的方法茵肃。PrintDescription
把傳入的對象的詳細描述打印,展示了通過-class
方法獲得的對象的類袭祟,也展示了通過OCruntime函數(shù)獲得的對象的類以及這個類上實現(xiàn)的方法验残。
接下來我們開始試驗這些功能。我們創(chuàng)建了TestClass
的四個實例巾乳,每一個都用不同的方法進行觀察您没。x
實例觀察x的key,y
也類似胆绊,xy
將會同時觀察x和y的key氨鹏。z 的key不進行觀察以進行對比。最后control
實例作為一個實驗中的控制變量压状,將不會進行觀察仆抵。
接下來我們打印出四個對象的描述。
接下來我們更深入的挖掘一下重寫的setter何缓,將control對象的和被觀察對象的-setX:
方法的實現(xiàn)的地址打印出來肢础,進行對比。我們做兩次這個操作碌廓,因為使用-methodForSelector:
不能展示出重寫传轰。KVO期望隱藏動態(tài)繼承,甚至希望用這個技術(shù)隱藏重寫的方法谷婆!但是使用OCruntime函數(shù)卻可以輸出合理的結(jié)果慨蛙。
運行代碼
所以說以上就是代碼所做的事。我們來看一下運行的結(jié)果:
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對象纪挎。與期望相符的是期贫,它所屬的類是TestClass
,并且也實現(xiàn)了我們從類的屬性所同步來的六個方法异袄。
接下來打印了三個被觀察的對象通砍。注意到:盡管-class
方法還是顯示TestClass
,使用object_getClass
卻展現(xiàn)這個對象的真實面貌:它是一個NSKVONotifying_TestClass
對象。這就是動態(tài)繼承的子類封孙!
來看一下它是怎么實現(xiàn)兩個被觀察的setter的迹冤。你會注意到不重寫-setZ:
是很聰明的,因為即使它也是一個setter虎忌,但是并沒有人觀察它泡徙。我們可以推測,如果我們給z也添加一個觀察者膜蠢,那么NSKVONotifying_Class
也會給-setZ:
進行重寫堪藐。同時也要注意三個實例同屬一個類,也就是說它們都重寫了兩個setter挑围,盡管他們兩個都只被觀察了一個屬性礁竞。即使對于未觀察的屬性也調(diào)用被觀察的setter,這樣會有性能損耗贪惹,但是Apple顯然覺得:如果每個對象都有一組不同的key被觀察苏章,不生成多個動態(tài)繼承的子類是更合適的。我也覺得這樣是對的奏瞬。
你也會觀察到三個其他的方法枫绅。這就是前面提到的被重寫的-class
方法,這個方法希望隱藏掉這個動態(tài)子類的存在硼端。還有一個-dealloc
方法來進行清理并淋。另外還有一個神秘的-_isKVOA
方法,這個方法看起來像是一個私有方法珍昨,Apple的代碼可以使用這個方法來確定一個獨享是不是服從動態(tài)繼承县耽。
接下來我們打印出了-setX:
的實現(xiàn)。使用-methodForSelector:
給兩者返回了相同的值镣典。既然在動態(tài)子類中不存在這個方法的重寫兔毙,這肯定意味著-methodForSelector:
使用了-class
作為內(nèi)部實現(xiàn)的一部分,也因此獲得了錯誤的結(jié)果兄春。
所以我們當然就略過這些東西澎剥,使用了OCruntime來打印出了實現(xiàn),然后我們就能看到不同了赶舆。原始的方法和-methodForSelector:
相同哑姚,但第二個完全不同。
作為優(yōu)秀的探索者芜茵,我們在debugger中運行叙量,這樣我們可以清晰的看到第二個函數(shù)是什么:
(gdb) print (IMP)0x96a1a550
$1 = (IMP) 0x96a1a550 <_NSSetIntValueAndNotify>
這是某種私有方法,實現(xiàn)了觀察的通知工作九串。通過使用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
這個列表中我們可以發(fā)現(xiàn)很多有趣的東西。首先你可能看到了Apple不得不為它們想支持的每一個原始類型都實現(xiàn)了獨立的函數(shù)征炼。它們對于OC對象(_NSSetObjectValueAndNotify
)只需要一個方法析既,但是需要對其余的部分實現(xiàn)一整套方法。這套方法好像不太完整谆奥,其中不包含long
double
_Bool
的方法。甚至對普通指針類型也沒有方法拂玻,就比如CFTypeDef
的酸些。盡管對于不同的常見Cocoa structs有幾個函數(shù),顯然這里并沒有其他特別多的structs的函數(shù)檐蚜。這意味著這些類型的屬性不能進行自動的KVO通知魄懂,一定要記住4车凇J欣酢!
KVO是一個很強大的技術(shù)咳短,有時候可能有點過于強大了填帽,尤其是當涉及到自動通知的時候。現(xiàn)在你很清楚它內(nèi)部是怎么工作的了咙好,這個認知會幫助你決定怎么使用它(KVO)篡腌,或者在出錯的時候幫助你進行debug。
如果你準備在你自己的應(yīng)用中使用KVO勾效,你也許想看看我的文章Key-Value Observing Done Right嘹悼。
總結(jié)
以下并非關(guān)鍵內(nèi)容,大地瓜不翻譯了层宫。