原文地址:https://cocoasamurai.blogspot.jp/2010/01/understanding-objective-c-runtime.html
原文作者:Colin Wheeler
引子
當(dāng)人們談到Objective-C/Cocoa時(shí)咖气,Objective-C Runtime是一個(gè)非常容易被忽略掉的特點(diǎn)迟隅,這大概是因?yàn)镺C語言本身是一門可以在一小段時(shí)間就能入門的語言砸西,學(xué)習(xí)Cocoa的新手們往往也只是在Cocoa框架和怎么使用框架上埋頭鉆研刑赶。可是顿颅,OC Runtime的工作原理是每一個(gè)學(xué)習(xí)OC的人都至少應(yīng)該了解的饰及,而并不是僅限于知道[target doMethodWith:var1];
在經(jīng)過編譯之后變成了objc_msgSend(target,@selector(doMethodWith:),var1);
這么表層的東西捉邢。理解了OC Runtime之后,有助于我們更深入的理解OC語言本身和應(yīng)用的運(yùn)行機(jī)制吮螺。在我看來饶囚,不論開發(fā)經(jīng)驗(yàn)多或少,每一位Mac/iOS開發(fā)者都能從本文獲取到一點(diǎn)新的知識鸠补。
OC Runtime開源項(xiàng)目
OC Runtime是一項(xiàng)開源項(xiàng)目萝风,你可以從http://opensource.apple.comn 隨時(shí)獲得源代碼。實(shí)際上紫岩,當(dāng)初我剛開始去探究OC Runtime的工作機(jī)制時(shí)规惰,相反,正是通過閱讀它的源代碼泉蝌,而不是官方文檔開始的歇万。你也可以通過以下鏈接下載針對Mac OS X 10.6.2版本的Runtime源代碼objc4-437.1.tar.gz。
動(dòng)態(tài)語言VS靜態(tài)語言
OC是一門運(yùn)行時(shí)才定向的語言勋陪,這意味著到底該由哪個(gè)對象來執(zhí)行消息是在經(jīng)過編譯贪磺、鏈接之后,待運(yùn)行時(shí)才會被確定下來诅愚。這種動(dòng)態(tài)性給我們提供了巨大而又靈活的應(yīng)用空間寒锚,我們可以利用這個(gè)特點(diǎn)來實(shí)現(xiàn)一些平常不容易做到的操作,例如到運(yùn)行時(shí)再將消息轉(zhuǎn)發(fā)給需要其處理的對象违孝,甚至還能交換兩個(gè)對象的實(shí)現(xiàn)方法壕曼。正是由于這種靈活的動(dòng)態(tài)性,我們在使用Runtime時(shí)等浊,應(yīng)該仔細(xì)檢查每個(gè)對象所能處理或者不能處理但能正確轉(zhuǎn)發(fā)的消息腮郊。而與此對應(yīng)的C語言的運(yùn)行機(jī)制則是:從main()
函數(shù)開始,之后從上到下筹燕,按照寫好的代碼邏輯轧飞,順序執(zhí)行我們構(gòu)造的功能函數(shù),而且C語言中的結(jié)構(gòu)體無法將函數(shù)的調(diào)用進(jìn)行轉(zhuǎn)發(fā)撒踪,例如如下所示的一段C語言代碼:
#include < stdio.h >
int main(int argc, const char **argv[])
{
printf("Hello World!");
return 0;
}
在經(jīng)過編譯器編譯之后过咬,會變成如下的匯編代碼:
.text
.align 4,0x90
.globl _main
_main:
Leh_func_begin1:
pushq %rbp
Llabel1:
movq %rsp, %rbp
Llabel2:
subq $16, %rsp
Llabel3:
movq %rsi, %rax
movl %edi, %ecx
movl %ecx, -8(%rbp)
movq %rax, -16(%rbp)
xorb %al, %al
leaq LC(%rip), %rcx
movq %rcx, %rdi
call _printf
movl $0, -4(%rbp)
movl -4(%rbp), %eax
addq $16, %rsp
popq %rbp
ret
Leh_func_end1:
.cstring
LC:
.asciz "Hello World!"
然后又經(jīng)過鏈接,最終會生成一個(gè)可執(zhí)行文件制妄。這個(gè)過程和OC依賴OC Runtime庫編譯掸绞、鏈接程序的過程類似。例如下邊一段OC代碼:
[self doSomethingWithVar:var1];
會被編譯成:
objc_msgSend(self,@selector(doSomethingWithVar:),var1);
除了這一點(diǎn),我們對于OC的Runtime的工作機(jī)制完全不知道衔掸。
什么是OC Runtime
OC Runtime是一個(gè)Runtime庫烫幕,它主要以C和匯編語言為基礎(chǔ),使用面向?qū)ο蟮腛C來編寫敞映。這意味著较曼,它可以加載類,也可以對消息進(jìn)行轉(zhuǎn)發(fā)振愿、分發(fā)等捷犹。總而言之冕末,OC Runtime為OC這門面向?qū)ο蟮恼Z言提供了基礎(chǔ)性的結(jié)構(gòu)支持萍歉。
OC Runtime相關(guān)術(shù)語
在我們進(jìn)行更深一步的探討之前,先讓我們共同梳理一下關(guān)于OC Runtime的術(shù)語档桃。
1.Runtime
Mac/iOS開發(fā)人員關(guān)心的有2個(gè)運(yùn)行時(shí):現(xiàn)代運(yùn)行時(shí)(Modern Runtime)和傳統(tǒng)運(yùn)行時(shí)(Legacy Runtime)∏购ⅲ現(xiàn)代運(yùn)行時(shí)涵蓋了所有64位的Mac OS X和iPhone應(yīng)用,而傳統(tǒng)運(yùn)行時(shí)則包括剩下的所有的32位Mac OS X應(yīng)用胳蛮。
2.方法(Methods)
包括兩類基本的方法:對象方法(以"-"
開頭销凑,像- (void)doFoo
);類方法(以"+"
開頭,像+(id)alloc
)仅炊。方法看起來和C語言中的函數(shù)很像斗幼,它們內(nèi)部都是一段需要執(zhí)行的語句,像下面這樣:
-(NSString *)movieTitle
{
return @"Futurama: Into the Wild Green Yonder";
}
3.選擇器(selector)
OC中的選擇器有點(diǎn)類似于C語言中的數(shù)據(jù)結(jié)構(gòu)體抚垄,它扮演著確定需要執(zhí)行的OC方法的角色蜕窿。在Runtime中它的定義類似于下面這樣:
typedef struct objc_selector *SEL;
使用時(shí):
SEL aSel = @selector(movieTitle);
4.消息(Message)
[target getMovieTitleForObject:obj];
OC中的消息其實(shí)就是2個(gè)中括號中間的東西。它由接受消息的目標(biāo)對象呆馁、需要執(zhí)行的方法名和需要傳入的參數(shù)三部分組成桐经。OC消息類似于C語言的函數(shù),但它們又不相同浙滤,給一個(gè)目標(biāo)對象發(fā)送消息并不代表該對象就一定會執(zhí)行這個(gè)方法阴挣,接受對象可以根據(jù)消息的發(fā)送者決定具體執(zhí)行的方法,或者是該對象本身并不執(zhí)行纺腊,而是將此消息轉(zhuǎn)發(fā)給另一個(gè)對象去執(zhí)行畔咧。
5.類(class)
在Runtime中,我們可以發(fā)現(xiàn)如下定義:
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
這里面?zhèn)鬟_(dá)出幾點(diǎn)信息揖膜。首先這段代碼中分別有類的結(jié)構(gòu)體定義和對象的結(jié)構(gòu)體定義誓沸;其次對象的結(jié)構(gòu)體中有一個(gè)類指針isa
,也就是我們平常所說的“isa
指針”壹粟。isa指針的存在是為了在運(yùn)行時(shí)檢查一個(gè)對象的父類拜隧,然后在其父類對應(yīng)的類方法列表中查詢可以響應(yīng)消息的方法。最后,代碼的結(jié)尾處還有一個(gè)id指針洪添。id指針默認(rèn)是告訴我們這個(gè)對象只是一個(gè)OC對象垦页,系統(tǒng)可以通過查詢id指針指向?qū)ο笏鶎俚念悾M(jìn)而查詢到它能否對消息做出響應(yīng)薇组。當(dāng)然外臂,如果id指針指向的對象一經(jīng)確定坐儿,我們可以進(jìn)行更多的操作律胀。
6.塊(Blocks)
在LLVM/Clang文檔中,關(guān)于blocks的介紹:
struct Block_literal_1 {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src);
void (*dispose_helper)(void *src);
} *descriptor;
// imported variables
};
代碼塊(blocks)是用來與OC Runtime相配合的貌矿,它也可看作是OC對象炭菌,所以也能對retain、release逛漫、copy等消息做出響應(yīng)黑低。
6.IMP實(shí)現(xiàn)方法(Method Implementations)
typedef id (*IMP)(id self,SEL _cmd,...);
IMP是編譯器為我們生成的指向?qū)崿F(xiàn)方法的指針。如果是剛開始接觸OC語言酌毡,并不需要了解IMP克握,不過我們現(xiàn)在討論的是Runtime,稍后就可以看到Runtime中是如何執(zhí)行IMP的枷踏。
7.OC類(Objective-C Classes)
OC Classes中具體都有什么呢菩暗?一個(gè)最基本的類的實(shí)現(xiàn)就像下邊這樣:
@interface MyClass : NSObject {
//vars
NSInteger counter;
}
//methods
-(void)doFoo;
@end
但是在Runtime中,系統(tǒng)需要記錄更多信息:
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
可以看到旭蠕,一個(gè)OC類有對以下對象的引用:它的父類停团、它的名稱、實(shí)例變量掏熬、方法佑稠、緩存以及協(xié)議。當(dāng)一個(gè)OC類或一個(gè)OC對象需要對消息做出響應(yīng)時(shí)旗芬,Runtime需要上述信息才能完成工作舌胶。
是類還是其本身定義一個(gè)對象?如何實(shí)現(xiàn)的疮丛?
我在之前說過幔嫂,其實(shí)類也是對象。Runtime會將類看作是元類(MetaClass)的對象來處理这刷。當(dāng)我們向類發(fā)送一個(gè)消息婉烟,類似于[NSObject alloc]
這樣,其實(shí)類是作為元類的一個(gè)對象來接收消息的暇屋。同時(shí)似袁,元類又可看作是根元類(RootMetaClass)的對象,例如一個(gè)繼承自NSObject
的類,它的類指針指向NSObject
昙衅。所有的元類的類指針都指向根元類作為其父類扬霜,而且元類中還保存著其能做出響應(yīng)的方法列表,當(dāng)我們給一個(gè)類發(fā)送消息而涉,就像[NSObject alloc]
著瓶,其實(shí)是objc_msgSend()
在NSObjec
的元類的方法列表中查詢能夠?qū)?code>alloc做出相應(yīng)的方法,然后讓其執(zhí)行啼县。
為什么我們要繼承自蘋果的類庫材原?
剛開始接觸Cocoa開發(fā)時(shí),一般總會從繼承NSObject類開始寫代碼季眷。繼承蘋果的類庫可以給我們的開發(fā)帶來的極大方便余蟹,我們也享受著這種方便。令人驚奇的是子刮,我們與Runtime打交道威酒,其實(shí)這時(shí)候就開始了。當(dāng)我們給自定義的類創(chuàng)建一個(gè)實(shí)例對象時(shí)挺峡,一般會這樣做:
MyObject *object = [[MyObject alloc] init];
+alloc
是第一個(gè)被執(zhí)行的消息葵孤。在這個(gè)文檔中,它是這樣介紹這個(gè)過程的:新建對象的isa指針依據(jù)類的數(shù)據(jù)結(jié)構(gòu)進(jìn)行初始化橱赠,開辟新內(nèi)存尤仍,將對象中其余的變量值置0。所以病线,繼承蘋果的類庫時(shí)吓著,我們不僅繼承了一些很好的功能,同時(shí)也繼承了類似上述可以輕松創(chuàng)建對象并初始化的過程送挑,而且通過這個(gè)操作創(chuàng)建出來的對象都能和Runtime要求的數(shù)據(jù)結(jié)構(gòu)相一致(例如對象的isa指針會自動(dòng)指向自定義的類)绑莺。
什么是類緩存(Class Cache)?
通過對對象的isa指針進(jìn)行追蹤惕耕,Runtime可以找到對象所能響應(yīng)的所有方法纺裁。然而我們經(jīng)常調(diào)用的卻往往只是這些方法中的一小部分,所以對象在響應(yīng)消息時(shí)司澎,就沒必要每次都查詢所有的方法欺缘,類緩存的概念也因此而來。類緩存的工作原理大致是:當(dāng)對象對一個(gè)消息做出響應(yīng)時(shí)挤安,系統(tǒng)就會將這個(gè)方法存入到類緩存中谚殊,等objc_msgSend()
下次查詢時(shí),就會優(yōu)先檢查類緩存蛤铜,因?yàn)橄到y(tǒng)會認(rèn)為調(diào)用完一個(gè)方法之后很有可能下次會再調(diào)用相同的方法嫩絮。讓我們以此為基礎(chǔ)思考一下以下代碼的執(zhí)行過程:
MyObject *obj = [[MyObject alloc] init];
@implementation MyObject
-(id)init {
if(self = [super init]){
[self setVarA:@”blah”];
}
return self;
}
@end
上述代碼大致分別執(zhí)行了以下過程:
-
[MyObject alloc]
最先執(zhí)行丛肢,然而MyObject類并沒有+alloc
對應(yīng)的實(shí)現(xiàn)方法,所以系統(tǒng)接著就會查詢MyObject的父類——NSObject剿干; - 經(jīng)過查詢蜂怎,NSObject可以對
+alloc
做出響應(yīng),接著系統(tǒng)會檢查MyObjec類置尔,并在內(nèi)存中開辟一塊與其數(shù)據(jù)結(jié)構(gòu)相一致的內(nèi)存杠步,并將isa指針指向MyObject,完成對象的創(chuàng)建過程榜轿,并把+alloc
存入NSObject對應(yīng)的類緩存中幽歼; - 到目前為止,系統(tǒng)執(zhí)行的都還是類方法差导,接下來就該執(zhí)行
-init
或者其他初始化操作的對象方法了试躏,同樣系統(tǒng)也會將-init
方法存入到類緩存中猪勇; - 接下來該執(zhí)行
self = [super init]
了设褐。super是一個(gè)指向父類的關(guān)鍵字,在這里泣刹,系統(tǒng)會查詢NSObject的方法列表并執(zhí)行其中的init
方法助析。這一步的操作是為了確保OOP繼承模型能正確工作,其原理大致是:首先應(yīng)該正確初始化父類的相關(guān)變量椅您,然后我們自定義的類(也就是子類)的變量才能夠得到正確的初始化外冀。如果有需要,我們也可以重寫父類掀泳。不過在這個(gè)例子中雪隧,NSObject似乎沒什么太大的作用,當(dāng)然這只是特殊情況员舵,有些時(shí)候脑沿,NSObject則承擔(dān)著十分重要的初始化角色,例如:
#import < Foundation/Foundation.h>
@interface MyObject : NSObject
{
NSString *aString;
}
@property(retain) NSString *aString;
@end
@implementation MyObject
-(id)init
{
if (self = [super init]) {
[self setAString:nil];
}
return self;
}
@synthesize aString;
@end
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
id obj1 = [NSMutableArray alloc];
id obj2 = [[NSMutableArray alloc] init];
id obj3 = [NSArray alloc];
id obj4 = [[NSArray alloc] initWithObjects:@"Hello",nil];
NSLog(@"obj1 class is %@",NSStringFromClass([obj1 class]));
NSLog(@"obj2 class is %@",NSStringFromClass([obj2 class]));
NSLog(@"obj3 class is %@",NSStringFromClass([obj3 class]));
NSLog(@"obj4 class is %@",NSStringFromClass([obj4 class]));
id obj5 = [MyObject alloc];
id obj6 = [[MyObject alloc] init];
NSLog(@"obj5 class is %@",NSStringFromClass([obj5 class]));
NSLog(@"obj6 class is %@",NSStringFromClass([obj6 class]));
[pool drain];
return 0;
}
上述代碼的打印結(jié)果會是什么呢马僻?如果你是剛接觸Cocoa開發(fā)庄拇,有可能會這樣回答:
NSMutableArray
NSMutableArray
NSArray
NSArray
MyObject
MyObject
而實(shí)際卻是這樣:
obj1 class is __NSPlaceholderArray
obj2 class is NSCFArray
obj3 class is __NSPlaceholderArray
obj4 class is NSCFArray
obj5 class is MyObject
obj6 class is MyObject
原因是:在OC中,+alloc
會返回一個(gè)類的對象韭邓,而-init
則會返回另一個(gè)類對象措近。
objc_msgSend()執(zhí)行了什么?
執(zhí)行了很多過程女淑,讓我們還是以一個(gè)例子開始:
[self printMessageWithString:@"HelloWorld!"];
經(jīng)過編譯之后瞭郑,上述代碼實(shí)際會變成:
objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");
通過對isa指針的跟蹤,系統(tǒng)會查詢接受對象(或其父類)能否對選擇器(selector)做出響應(yīng)鸭你,假如系統(tǒng)在類的分發(fā)表(class dispatch table)或者類緩存中屈张,找到了對應(yīng)方法我抠,則系統(tǒng)就會跳轉(zhuǎn)到對應(yīng)方法的地址并開始執(zhí)行。不過objc_msgSend()并不會返回消息袜茧,它開始執(zhí)行之后只是通過指針找到對應(yīng)的實(shí)現(xiàn)方法菜拓,然后由實(shí)現(xiàn)方法執(zhí)行并完成返回,這看起來就跟是objc_msgSend()返回一樣笛厦。關(guān)于這一點(diǎn)纳鼎,Bill通過三部分(part1、part2&part3)講解的更為細(xì)致裳凸,他的意思大致是:在OC代碼中贱鄙,
- 檢查是否有可以被忽略或者被繞過不執(zhí)行的選擇器——顯然,在運(yùn)行有垃圾回收的機(jī)制下姨谷,類似于
-retain/-release
的操作都可以被忽略掉了逗宁; - 檢查有沒有空(nil)對象。OC語言與其他語言不同梦湘,給nil對象發(fā)送消息完全合法瞎颗,并且有些時(shí)候你也愿意這么做,不過在這里我們假設(shè)接收對象不為空捌议,接下來……
- 系統(tǒng)在類中查詢實(shí)現(xiàn)方法(IMP)哼拔。首先在類緩存中查詢,如果找到了就直接跳轉(zhuǎn)至對應(yīng)的方法瓣颅;
- 如果類緩存中沒有要找的方法倦逐,系統(tǒng)就會轉(zhuǎn)而查詢類的分發(fā)表,如果在表中找到了就直接跳轉(zhuǎn)至對應(yīng)的方法宫补;
- 如果在類緩存和分發(fā)表中都沒有查詢到對應(yīng)的實(shí)現(xiàn)方法檬姥,系統(tǒng)就會啟用消息轉(zhuǎn)發(fā)機(jī)制。這意味著你的代碼將會被編譯器轉(zhuǎn)換為C語言中的函數(shù)粉怕。假如你寫了這樣一個(gè)方法:
-(int)doComputeWithNum:(int)aNum
它將會被轉(zhuǎn)換為:
int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum)
OC Runtime通過函數(shù)指針來調(diào)用你的函數(shù)健民,而你卻不能直接調(diào)用這些被轉(zhuǎn)換后的函數(shù)。不過Cocoa框架給我們提供了另外一個(gè)能獲取到這些函數(shù)指針的方法:
//聲明一個(gè)C的函數(shù)指針
int (computeNum *)(id,SEL,int);
//Cocoa中而不是OC Runtime中的方法
//取得和 objc_msgSend() 獲取到的一樣的函數(shù)指針
computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)];
//執(zhí)行Runtime返回的函數(shù)指針
computeNum(obj,@selector(doComputeWithNum:),aNum);
通過這種方式斋荞,你可以直接獲取到函數(shù)并在Runtime中調(diào)用它荞雏,甚至可以以此繞過Runtime的動(dòng)態(tài)機(jī)制,達(dá)到確定想執(zhí)行某一個(gè)方法的目的平酿。其實(shí)凤优,OC Runtime也是通過這種方法來獲取函數(shù)的地址的,只不過它是利用objc_msgSend()而已蜈彼。
OC消息轉(zhuǎn)發(fā)
在OC中筑辨,給一個(gè)不確定其能否做出響應(yīng)的對象發(fā)送消息是合法的,甚至有時(shí)會故意這樣做幸逆,對此棍辕,蘋果給出的解釋是:為了模擬OC本身并不支持的多繼承的特性暮现。這一點(diǎn)也是Runtime機(jī)制所必須的,它的工作原理大致如下:
- Runtime首先依次查詢類緩存楚昭、分發(fā)表及所有的父類(類緩存及分發(fā)表)栖袋,如果找不到對應(yīng)的方法就會執(zhí)行下一個(gè)步驟;
- OC Runtime會調(diào)用自定義類中的
+(BOOL)resolveInstanceMethod:(SEL)aSel
方法抚太,這是系統(tǒng)給我們的第一次補(bǔ)救的機(jī)會塘幅,通過實(shí)現(xiàn)上述方法我們可以在系統(tǒng)啟用消息轉(zhuǎn)發(fā)機(jī)制的第一步就告訴Runtime我們已經(jīng)做出補(bǔ)救,具體實(shí)現(xiàn)時(shí)首先應(yīng)定義一個(gè)函數(shù):
void fooMethod(id obj, SEL _cmd)
{
NSLog(@"Doing Foo");
}
然后將其添加到類方法:
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(doFoo:)){
class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
return YES;
}
return [super resolveInstanceMethod];
}
class_addMethod
最后的“v@”代表函數(shù)的返回類型和參數(shù)尿贫,可以通過Runtime手冊中的TypeEncodings來查看具體字符代表的含義电媳;
- 如果第2步中的補(bǔ)救沒有解決問題,系統(tǒng)會給我們提供第二次機(jī)會處理無法解決的方法庆亡。這一步仍要比接下來的措施好一點(diǎn)匾乓,因?yàn)楹罄m(xù)的補(bǔ)救措施將會更耗資源,原因在于下一個(gè)補(bǔ)救措施中將會創(chuàng)建新對象又谋,并執(zhí)行:
(void)forwardInvocation:(NSInvocation *)anInvocation;
不過在這一步中拼缝,我們可以這樣實(shí)現(xiàn):
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
顯然,不能再在這個(gè)方法里返回self搂根,這會引起死循環(huán)珍促。
- 如果上述方法都不能解決問題,Runtime會嘗試最后一次機(jī)會剩愧,調(diào)用
(void)forwardInvocation:(NSInvocation *)anInvocation;
NSInvocation是消息封裝后的對象,在系統(tǒng)創(chuàng)建出NSInvocation對象之后娇斩,我們可以改變消息的接收對象仁卷、選擇器和參數(shù),就像這樣:
-(void)forwardInvocation:(NSInvocation *)invocation
{
SEL invSEL = invocation.selector;
if([altObject respondsToSelector:invSEL]) {
[invocation invokeWithTarget:altObject];
} else {
[self doesNotRecognizeSelector:invSEL];
}
}
void fooMethod(id obj, SEL _cmd)
{
NSLog(@"Doing Foo");
}
然后將其添加到類方法:
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(doFoo:)){
class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
return YES;
}
return [super resolveInstanceMethod];
}
class_addMethod
最后的“v@”代表函數(shù)的返回類型和參數(shù)犬第,可以通過Runtime手冊中的TypeEncodings來查看具體字符代表的含義锦积;如果自定義的類是繼承自NSObject,則應(yīng)實(shí)現(xiàn)的方法是:- (void)forwardInvocation:(NSInvocation *)anInvocation
歉嗓。最后丰介,我們可以重寫-doesNotRecognizeSelector:
方法來做最后一點(diǎn)能做的事情,因?yàn)橄乱徊匠绦蚓蜁罎ⅰ?/p>
譯者注:這一步鉴分,一定要實(shí)現(xiàn)-methodSignatureForSelector:
這個(gè)方法哮幢,返回函數(shù)的簽名類型,即上一步中提到的“v@”志珍,否則-(void)forwardInvocation
不會執(zhí)行橙垢!
并不脆弱的變量(現(xiàn)代運(yùn)行時(shí),ModernRuntime)
我們最近才從現(xiàn)代運(yùn)行時(shí)中認(rèn)識到的一點(diǎn):并不脆弱的變量(Non Fragile ivars)伦糯。編譯時(shí)柜某,我們定義的變量是以在類中的偏移地址訪問的嗽元,而且這些工作編譯器能自動(dòng)幫我們完成,這牽扯到底層的細(xì)節(jié)喂击,大致類似于:先得到一個(gè)指針指向創(chuàng)建的對象剂癌,然后基于該對象的起始地址,再根據(jù)變量的偏移地址我們就可以訪問到變量翰绊,最后根據(jù)變量的類型確定變量所占的內(nèi)存空間珍手,所以編譯后變量的輸出形式(ivar layout)類似于下邊的表格,左邊一列數(shù)字代表偏移地址:
在蘋果給出Mac OS X10.x更新之前辞做,這一直都運(yùn)行良好琳要,可在更新之后,我們自定義的類中因?yàn)橛行┎糠峙c父類發(fā)生了重疊秤茅,重疊的部分會被系統(tǒng)擦除稚补,
不過在“不脆弱變量”的機(jī)制下,又發(fā)生了什么呢放仗?
這種情況下润绎,編譯器會自動(dòng)生成與“脆弱變量”機(jī)制下完全一樣的布局,不過當(dāng)Runtime檢測到與父類有重疊的部分時(shí)诞挨,它會在自定義類中自動(dòng)調(diào)整變量的偏移地址莉撇,從而保存自定義類的變量。
OC關(guān)聯(lián)對象
在Mac OS X10.6中新引入的一個(gè)名詞是——關(guān)聯(lián)引用惶傻。OC并不支持像其他語言中的動(dòng)態(tài)添加變量的功能棍郎,所以在這之前,我們不得不努力為將來有可能用到的變量預(yù)留出足夠的空間银室,而從Mac OS X10.6開始涂佃,OC已經(jīng)原生支持這一點(diǎn)了。假如我們想為現(xiàn)有的類蜈敢,比如NSView添加變量辜荠,可以:
#import < Cocoa/Cocoa.h> //Cocoa
#include < objc/runtime.h> //objc runtime api’s
@interface NSView (CustomAdditions)
@property(retain) NSImage *customImage;
@end
@implementation NSView (CustomAdditions)
static char img_key; //has a unique address (identifier)
-(NSImage *)customImage
{
return objc_getAssociatedObject(self,&img_key);
}
-(void)setCustomImage:(NSImage *)image
{
objc_setAssociatedObject(self,&img_key,image,
OBJC_ASSOCIATION_RETAIN);
}
@end
在Runtime.h中我們可以找到傳遞給objc_setAssociatedObject()
的選項(xiàng),
/* Associated Object support. */
/* objc_setAssociatedObject() options */
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
objc_setAssociatedObject()
的參數(shù)和@property
類似扶认。
混合vTable分發(fā)
在現(xiàn)代運(yùn)行時(shí)的源代碼中侨拦,有以下代碼:
/***********************************************************************
* vtable dispatch
*
* Every class gets a vtable pointer. The vtable is an array of IMPs.
* The selectors represented in the vtable are the same for all classes
* (i.e. no class has a bigger or smaller vtable).
* Each vtable index has an associated trampoline which dispatches to
* the IMP at that index for the receiver class's vtable (after
* checking for NULL). Dispatch fixup uses these trampolines instead
* of objc_msgSend.
* Fragility: The vtable size and list of selectors is chosen at launch
* time. No compiler-generated code depends on any particular vtable
* configuration, or even the use of vtable dispatch at all.
* Memory size: If a class's vtable is identical to its superclass's
* (i.e. the class overrides none of the vtable selectors), then
* the class points directly to its superclass's vtable. This means
* selectors to be included in the vtable should be chosen so they are
* (1) frequently called, but (2) not too frequently overridden. In
* particular, -dealloc is a bad choice.
* Forwarding: If a class doesn't implement some vtable selector, that
* selector's IMP is set to objc_msgSend in that class's vtable.
* +initialize: Each class keeps the default vtable (which always
* redirects to objc_msgSend) until its +initialize is completed.
* Otherwise, the first message to a class could be a vtable dispatch,
* and the vtable trampoline doesn't include +initialize checking.
* Changes: Categories, addMethod, and setImplementation all force vtable
* reconstruction for the class and all of its subclasses, if the
* vtable selectors are affected.
**********************************************************************/
這背后的思想是:vTable中保存著最經(jīng)常被調(diào)用的選擇器,因?yàn)檫@是用比objc_msgSend()更少的指令辐宾,所以可以提高應(yīng)用的運(yùn)行速度狱从。在vTable中保存著16個(gè)最經(jīng)常被調(diào)用的選擇器膨蛮,再往下,我們會看到在默認(rèn)的有垃圾回收機(jī)制的vTable和沒有開啟垃圾回收機(jī)制的vTable:
static const char * const defaultVtable[] = {
"allocWithZone:",
"alloc",
"class",
"self",
"isKindOfClass:",
"respondsToSelector:",
"isFlipped",
"length",
"objectForKey:",
"count",
"objectAtIndex:",
"isEqualToString:",
"isEqual:",
"retain",
"release",
"autorelease",
};
static const char * const defaultVtableGC[] = {
"allocWithZone:",
"alloc",
"class",
"self",
"isKindOfClass:",
"respondsToSelector:",
"isFlipped",
"length",
"objectForKey:",
"count",
"objectAtIndex:",
"isEqualToString:",
"isEqual:",
"hash",
"addObject:",
"countByEnumeratingWithState:objects:count:",
};
可我們怎么知道自己是不是從vTable中調(diào)用了這些方法呢季研?調(diào)試時(shí)敞葛,我們會看到以下幾種方法:
objc_msgSend_fixup
:代表該方法并沒有從vTable中調(diào)用;
objc_msgSend_fixedup
:代表調(diào)用了一開始在vTable中現(xiàn)在卻已不存在的方法与涡;
objc_msgSend_vtable[0-15]
:代表調(diào)用了vTable中的某一個(gè)方法惹谐,后邊的數(shù)字代表該方法在vTable中的序號。
Runtime會自動(dòng)調(diào)整vTable中方法的順序驼卖,所以這次有可能objc_msgSend_vtable10
對應(yīng)著-length
方法氨肌,但下次運(yùn)行時(shí),不要指望它倆還是對應(yīng)著的酌畜。
總結(jié)
我希望你能喜歡這篇文章怎囚,這也是我在Des Moines Cocoaheads演講中的內(nèi)容。OC Runtime是一項(xiàng)浩大的工程桥胞,它為我們的Cocoa/OC應(yīng)用提供了動(dòng)力恳守,同時(shí)也讓我們習(xí)以為常的功能得以實(shí)現(xiàn),如果你還沒有瀏覽過蘋果的官方文檔贩虾,希望你能瀏覽一下催烘,以便能夠更好的利用OC Runtime。再次感謝你的閱讀缎罢!