[翻譯]理解Objective-C Runtime

原文地址: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í)行了以下過程:

  1. [MyObject alloc]最先執(zhí)行丛肢,然而MyObject類并沒有+alloc對應(yīng)的實(shí)現(xiàn)方法,所以系統(tǒng)接著就會查詢MyObject的父類——NSObject剿干;
  2. 經(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)的類緩存中幽歼;
  3. 到目前為止,系統(tǒng)執(zhí)行的都還是類方法差导,接下來就該執(zhí)行-init或者其他初始化操作的對象方法了试躏,同樣系統(tǒng)也會將-init方法存入到類緩存中猪勇;
  4. 接下來該執(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通過三部分(part1part2&part3)講解的更為細(xì)致裳凸,他的意思大致是:在OC代碼中贱鄙,

  1. 檢查是否有可以被忽略或者被繞過不執(zhí)行的選擇器——顯然,在運(yùn)行有垃圾回收的機(jī)制下姨谷,類似于-retain/-release的操作都可以被忽略掉了逗宁;
  2. 檢查有沒有空(nil)對象。OC語言與其他語言不同梦湘,給nil對象發(fā)送消息完全合法瞎颗,并且有些時(shí)候你也愿意這么做,不過在這里我們假設(shè)接收對象不為空捌议,接下來……
  3. 系統(tǒng)在類中查詢實(shí)現(xiàn)方法(IMP)哼拔。首先在類緩存中查詢,如果找到了就直接跳轉(zhuǎn)至對應(yīng)的方法瓣颅;
  4. 如果類緩存中沒有要找的方法倦逐,系統(tǒng)就會轉(zhuǎn)而查詢類的分發(fā)表,如果在表中找到了就直接跳轉(zhuǎn)至對應(yīng)的方法宫补;
  5. 如果在類緩存和分發(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)擦除稚补,


變量擦除
唯一的解決辦法是蘋果還會到以前的布局方式,不過如果蘋果這樣做的話框喳,意味著他們的框架就會因?yàn)槠渥兞慷x被凍結(jié)而不能與時(shí)俱進(jìn)课幕,在這種“脆弱變量”的機(jī)制下,我們不得不重新編譯五垮,以使自定義的類繼承到父類已經(jī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。再次感謝你的閱讀缎罢!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末伊群,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子屁使,更是在濱河造成了極大的恐慌在岂,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛮寂,死亡現(xiàn)場離奇詭異,居然都是意外死亡易茬,警方通過查閱死者的電腦和手機(jī)酬蹋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抽莱,“玉大人范抓,你說我怎么就攤上這事∈愁恚” “怎么了匕垫?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長虐呻。 經(jīng)常有香客問我象泵,道長寞秃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任偶惠,我火速辦了婚禮春寿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘忽孽。我一直安慰自己绑改,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布兄一。 她就那樣靜靜地躺著厘线,像睡著了一般。 火紅的嫁衣襯著肌膚如雪出革。 梳的紋絲不亂的頭發(fā)上杂曲,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機(jī)與錄音崩侠,去河邊找鬼后德。 笑死,一個(gè)胖子當(dāng)著我的面吹牛栖雾,可吹牛的內(nèi)容都是我干的楞抡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼析藕,長吁一口氣:“原來是場噩夢啊……” “哼召廷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起账胧,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤竞慢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后治泥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體筹煮,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年居夹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了败潦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡准脂,死狀恐怖劫扒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情狸膏,我是刑警寧澤沟饥,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響贤旷,放射性物質(zhì)發(fā)生泄漏广料。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一遮晚、第九天 我趴在偏房一處隱蔽的房頂上張望性昭。 院中可真熱鬧,春花似錦县遣、人聲如沸糜颠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽其兴。三九已至,卻和暖如春夸政,著一層夾襖步出監(jiān)牢的瞬間元旬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工守问, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留匀归,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓耗帕,卻偏偏與公主長得像穆端,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子仿便,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容