【譯】理解 Objective-C Runtime

原文地址:
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html
水平有限,如翻譯有誤還請指正只冻,謝謝~


剛接觸Objective-C(以下簡稱ObjC)的人很容易忽略它的一個(gè)特性——Rumtime(運(yùn)行時(shí))姑尺。原因是由于ObjC只需要幾小時(shí)就能學(xué)會(huì)同蜻,初學(xué)者往往花費(fèi)更多的時(shí)間在調(diào)整自己的程序使得其能夠適應(yīng)Cocoa框架的工作方式上妓羊。然而,runtime是每個(gè)人都應(yīng)該了解的,至少應(yīng)該知道它在一些細(xì)節(jié)上是如何工作的,比如知道[target doMethodWith:var1];這種代碼是會(huì)被編譯器翻譯成objc_msgSend(target,@selector(doMethodWith:),var1);的九昧。理解ObjC runtime的工作原理肯定會(huì)讓你對(duì)ObjeC語言本身以及app是如何運(yùn)行的有更深的理解绊袋。我想所有的Mac/iPhone的開發(fā)者,不管你是新手還是經(jīng)驗(yàn)豐富的老將铸鹰,都能從中受益癌别。

Objective-C Runtime是開源的

ObjC Runtime一直都是開源的:http://opensource.apple.com
ObjC Runtime源碼地址: http://www.opensource.apple.com/source/objc4/

動(dòng)態(tài)語言 vs 靜態(tài)語言

ObjC是一種由運(yùn)行時(shí)導(dǎo)向的語言,也就是說蹋笼,代碼具體要做的事在運(yùn)行時(shí)才會(huì)決定展姐,編譯鏈接階段決定要做的則不一定真的會(huì)執(zhí)行。這就給予了你很大的靈活度剖毯,比如如果你需要圾笨,你可以把一條消息重定向給一個(gè)你認(rèn)為合適的對(duì)象,甚至你可以直接調(diào)換兩個(gè)方法的實(shí)現(xiàn)逊谋,等等擂达。這就需要用到runtime的一些功能,檢查對(duì)象看看它能做什么或者不能做什么涣狗,然后將消息分發(fā)到恰當(dāng)?shù)膶?duì)象上谍婉。如果我們拿C語言對(duì)比來看,在C語言中镀钓,你的代碼從一個(gè)main()方法開始穗熬,然后就是從上往下執(zhí)行你所寫的邏輯和方法。C語言的方式不能將一個(gè)消息轉(zhuǎn)發(fā)給另一個(gè)對(duì)象丁溅。假如現(xiàn)在你有下面這段程序:

#include < stdio.h >
 
int main(int argc, const char **argv[])
{
        printf("Hello World!");
        return 0;
} 

編譯器會(huì)將你的代碼優(yōu)化唤蔗,然后轉(zhuǎn)換成匯編語言

.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!"

然后把它和庫鏈接起來,生成可執(zhí)行文件窟赏。然而類似的代碼在ObjC中妓柜,由于runtime庫的存在,編譯器所生成的代碼可能會(huì)不一樣涯穷。當(dāng)我們剛學(xué)ObjC的時(shí)候棍掐,可能會(huì)被告知類似的代碼在ObjC中類似于(簡單來說):

[self doSomethingWithVar:var1];

它會(huì)被編譯器翻譯成:

objc_msgSend(self,@selector(doSomethingWithVar:),var1);

但是除此之外,我們對(duì)于runtime到底做了什么就不知道了拷况。

Objective-C Runtime到底是什么作煌?

Objective-C Runtime是一個(gè)運(yùn)行時(shí)類庫,主要是用C語言和匯編寫的赚瘦,它是在C語言的基礎(chǔ)上增加了面向?qū)ο蟮哪芰λ谑模瑥亩鴦?chuàng)造了ObjC。加載類信息起意,分發(fā)和轉(zhuǎn)發(fā)所有的方法鹰服,等等都是它的工作。本質(zhì)上來說,就是因?yàn)橛辛藃untime悲酷,才使得用ObjC實(shí)現(xiàn)面向?qū)ο缶幊坛蔀榱丝赡堋?/p>

Objective-C Runtime 術(shù)語

進(jìn)一步深入了解runtime之前套菜,讓我們先看一些術(shù)語,這樣我才能確定你聽得懂我說的是什么设易。

  • 有2種runtime:現(xiàn)代的runtime(The Modern Runtime)和老的runtime(the Legacy Runtime)×龋現(xiàn)代的runtime覆蓋了所有64位的Mac OS X應(yīng)用程序和所有iPhone應(yīng)用程序。而老的runtime只覆蓋了所有32位Mac OS X應(yīng)用程序亡嫌。

  • 有2種基本類型的方法:實(shí)例方法(以-開頭的比如-(void)dofoo;)和類方法(以+開頭的比如+(id)alloc),方法就類似于C語言中的函數(shù)掘而,是一段代碼的集合挟冠,用來執(zhí)行一個(gè)小任務(wù),像下面這個(gè):

-(NSString *)movieTitle
{
    return @"Futurama: Into the Wild Green Yonder";
}
  • Selector(方法選擇器)
    ObjC中的Selector實(shí)際上是一個(gè)結(jié)構(gòu)體袍睡,用來標(biāo)識(shí)你想執(zhí)行的方法知染。在runtime中,它被定義為:
typedef struct objc_selector  *SEL; 

使用方式如下:

SEL aSel = @selector(movieTitle); 
  • Message(消息)
[target getMovieTitleForObject:obj];

ObjC里的消息就是包括在兩個(gè)中括號(hào)[]之間的東西斑胜,它包含了接受這個(gè)消息的目標(biāo)控淡,你想要執(zhí)行的方法,以及方法中傳遞的參數(shù)止潘。和C語言中的函數(shù)相似掺炭,但是調(diào)用方式不一樣。實(shí)際上你給一個(gè)對(duì)象發(fā)送的方法不一定會(huì)被執(zhí)行凭戴,因?yàn)榻邮艿较⒌膶?duì)象可能會(huì)檢查一下消息的發(fā)送來源涧狮,然后決定是否執(zhí)行另一個(gè)方法或者把這個(gè)接收到的消息轉(zhuǎn)發(fā)給另一個(gè)對(duì)象。

  • Class(類)
    如果你在runtime中尋找類的定義的話么夫,將會(huì)看到下面的代碼:
typedef struct objc_class *Class;
typedef struct objc_object {
    Class isa;
} *id; 

這里有一個(gè)ObjC類的結(jié)構(gòu)體者冤,和ObjC對(duì)象的結(jié)構(gòu)體。所有的ObjC對(duì)象都有一個(gè)定義為isa的類指針档痪,這就是我們通常所說的isa指針涉枫。運(yùn)行時(shí)就是用這個(gè)isa指針來檢查對(duì)象屬于哪個(gè)類,在發(fā)送消息時(shí)查看對(duì)象是否能響應(yīng)這個(gè)消息腐螟。
然后愿汰,我們再看id指針。id指針用來指向一個(gè)ObjC對(duì)象遭垛,當(dāng)你有一個(gè)id指針尼桶,你可以獲取它的類,查看其是否能夠響應(yīng)某個(gè)方法锯仪,如果你已經(jīng)知道了這是哪個(gè)對(duì)象泵督,還可以做一些更具體的操作。
你也可以在LLVM/Clang的文檔中找到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被設(shè)計(jì)成了能夠與ObjC runtime兼容庶喜,可以把它們當(dāng)做對(duì)象來看待小腊,它們也能夠響應(yīng)消息救鲤,比如-retain,-release,-copy,等等秩冈。

  • IMP(Method Implementations 方法實(shí)現(xiàn))
typedef id (*IMP)(id self,SEL _cmd,...); 

IMP是一個(gè)指向方法實(shí)現(xiàn)的函數(shù)指針本缠,如果你是一個(gè)初學(xué)者,你不需要直接去處理它入问,編譯器會(huì)幫你自動(dòng)生成丹锹。后面我將會(huì)說到運(yùn)行時(shí)如何調(diào)用方法的,其實(shí)執(zhí)行的都是這些函數(shù)指針指向的方法實(shí)現(xiàn)芬失。

  • ObjC Class
    說這么多楣黍,到底ObjC類長啥樣子呢?一個(gè)最基本的ObjC類看起來像這樣:
@interface MyClass : NSObject {
//vars
NSInteger counter;
}
//methods
-(void)doFoo;
@end

runtime中能得到更多的信息:

#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è)類持有了它的父類棱烂,名字租漂,成員屬性列表,方法列表颊糜,緩存(cache)哩治,協(xié)議列表等信息。當(dāng)在處理消息的時(shí)候衬鱼,這些信息都是runtime所需要的业筏。

所以類可以定義對(duì)象,然后它自己也是個(gè)對(duì)象鸟赫?這是怎么回事驾孔?

是的,前面我說過在ObjC中惯疙,類本身也是一個(gè)對(duì)象翠勉,為了處理這些,runtime創(chuàng)造了元類(Meta Classes)霉颠。當(dāng)你發(fā)送一個(gè)類似于[NSObject alloc]的方法時(shí)对碌,你就是在給一個(gè)類對(duì)象發(fā)送消息。每個(gè)類都是一個(gè)元類的對(duì)象蒿偎,元類又是根元類(root meta class)的對(duì)象朽们。就像你從NSObject類繼承了一個(gè)子類,這個(gè)類就會(huì)指向NSObject把它當(dāng)做父類一樣诉位,所有的元類都指向根元類并把其作為自己的父類骑脱。所有的元類只是簡單的保存了他的類對(duì)象方法列表中的方法,當(dāng)你發(fā)送一個(gè)類方法消息時(shí)(比如[NSObject alloc]苍糠,它會(huì)被編譯器翻譯成objc_msgSend())叁丧,會(huì)在元類保存的方法列表中查找是否有響應(yīng)該消息的方法,如有找到了,就會(huì)在對(duì)應(yīng)的類對(duì)象上執(zhí)行拥娄。

為什么我們要繼承蘋果的類

當(dāng)你剛接觸Cocoa開發(fā)的時(shí)候蚊锹,所有的教程都會(huì)告訴你需要去繼承蘋果的類然后再開始寫代碼,你也應(yīng)該或多或少的感受到了這樣做的好處稚瘾。但是有一件事你不知道的是牡昆,當(dāng)你繼承蘋果的類時(shí),你的類會(huì)創(chuàng)建成適應(yīng)ObjC runtime工作方式的類摊欠。在給我們自己的類創(chuàng)建實(shí)例的時(shí)候丢烘,我們大概會(huì)這么做:

MyObject *object = [[MyObject alloc] init];

第一個(gè)被執(zhí)行的方法是+alloc蘋果的官方文檔里說道

The isa instance variable of the new instance is initialized to a data structure that describes the class; memory for all other instance variables is set to 0.

新創(chuàng)建對(duì)象的isa實(shí)例變量會(huì)被初始化為一個(gè)描述其類型的數(shù)據(jù)結(jié)構(gòu)些椒,而其他的實(shí)例變量會(huì)被初始化為0铅协。

所以繼承自蘋果的類不僅僅是繼承了一些重要屬性,還繼承了在內(nèi)存中輕松分配創(chuàng)建我們的對(duì)象的能力摊沉,并且創(chuàng)建出來的對(duì)象的結(jié)構(gòu)符合runtime的期望(有一個(gè)isa指針指向我們的類)。

那什么是Class Cache呢痒给?(objc_cache *cache)

當(dāng)runtime沿著對(duì)象的isa指針進(jìn)行檢索時(shí)说墨,有時(shí)可能會(huì)發(fā)現(xiàn)一個(gè)對(duì)象實(shí)現(xiàn)了很多個(gè)方法。但是你可能只需要調(diào)用其中的幾個(gè)方法苍柏,如果每次調(diào)用都在所有的方法列表中查找一遍的話尼斧,那太費(fèi)勁了。Class Cache就是為了解決這個(gè)問題而誕生的试吁,當(dāng)你在一個(gè)個(gè)類的方法列表中找到了你要調(diào)用的方法棺棵,這個(gè)方法就會(huì)被放入Class Cache中,以便下次調(diào)用熄捍。objc_msgSend()在查找一個(gè)類的方法時(shí)也會(huì)優(yōu)先從Class Cache中找烛恤。這都建立在這個(gè)理論上:如果你調(diào)用了一個(gè)類的方法一次,那么之后你很有可能再調(diào)用這個(gè)方法余耽。知道了這點(diǎn)缚柏,我們來分析一下下面這段代碼發(fā)生了什么:

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類并沒有實(shí)現(xiàn)+alloc方法碟贾,所以我們在MyObject類中查找這個(gè)方法會(huì)找不到币喧。
  2. 然后順著MyObject類的父類指針,在其父類NSObject中找到了+alloc方法袱耽。+alloc方法檢查到消息的接受者是MyObject類杀餐,會(huì)根據(jù)類的大小分配一塊內(nèi)存區(qū)域,使其isa指針指向MyObject類朱巨,并把+alloc方法放入NSObject的Class Cache中史翘。于是我們就得到了一個(gè)MyObject類的實(shí)例對(duì)象。
  3. 然后-init方法被執(zhí)行,MyObejct類實(shí)現(xiàn)了這個(gè)方法恶座,方法被執(zhí)行搀暑,然后被加入MyObject的Class Cache。
  4. 然后是self = [super init],super是一個(gè)關(guān)鍵字跨琳,指向?qū)ο蟮母割愖缘悖跃褪钦{(diào)用NSObject的init方法。這是為了保證面向?qū)ο缶幊讨欣^承關(guān)系的正常運(yùn)行脉让,因?yàn)橹挥挟?dāng)父類初始化完成了它所有的成員變量之后桂敛,子類才能初始化自己的成員變量,或者重寫父類的成員變量溅潜。
    這是一個(gè)很簡單的過程术唬,方法里沒有執(zhí)行什么重要的任務(wù),但是下面這個(gè)例子就不一樣的了:
#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;
}

如果你是一個(gè)初學(xué)者滚澜,你猜測的輸出結(jié)果可能會(huì)像這樣:

NSMutableArray
NSMutableArray 
NSArray
NSArray
MyObject
MyObject

但是實(shí)際上粗仓,輸出結(jié)果如下:

obj1 class is __NSPlaceholderArray
obj2 class is NSCFArray
obj3 class is __NSPlaceholderArray
obj4 class is NSCFArray
obj5 class is MyObject
obj6 class is MyObject

這是因?yàn)樵贠bjC中允許+alloc方法返回一個(gè)類的對(duì)象,-init方法返回另一個(gè)類的對(duì)象设捐。

那么objc_msgSend中究竟發(fā)生了什么借浊?

在objc_msgSend確實(shí)發(fā)什么了很多事情,假如我們有這樣一段代碼:

[self printMessageWithString:@"Hello World!"];

它會(huì)被編譯器翻譯成這樣:

objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");

這個(gè)方法根據(jù)self對(duì)象的isa指針去查找這個(gè)對(duì)象的類或者父類能否響應(yīng)這個(gè)方法@selector(printMessageWithString:)萝招。假如在類的方法列表或者Class Cache中找到了該方法蚂斤,就根據(jù)方法的函數(shù)指針找到函數(shù)的實(shí)現(xiàn)并執(zhí)行它。但是objc_msgSend()并不會(huì)返回槐沼,它被執(zhí)行后就會(huì)根據(jù)指針找到響應(yīng)方法并執(zhí)行儿倒,方法返回了看起來就像是objc_msgSend()返回了炉奴。關(guān)于這個(gè)過程, Bill Bumgarner 在Part1Part2森篷,Part3中進(jìn)行了詳細(xì)的講解卢未。大概意思就是:

  1. 檢查是否有可以忽略的方法堂飞,比如在垃圾回收的環(huán)境下我們可以忽略-retain悔叽,-release等方法。
  2. 檢查目標(biāo)對(duì)象是否為nil周蹭。不像其他語言趋艘,在ObjectC中給一個(gè)nil對(duì)象發(fā)消息是完全可以的,并有時(shí)你會(huì)因?yàn)槟承┰蛳M@么去做凶朗,假設(shè)我們有一個(gè)非nil的目標(biāo)對(duì)象瓷胧,然后我們繼續(xù)...
  3. 然后我們需要在目標(biāo)類中查找IMP,首先在類的Class Cache中查找棚愤,如果找到了就順著指針找到要執(zhí)行的方法搓萧。
  4. 如果在Class Cache中沒有找到IMP杂数,那么就到類的方法列表中查找,如果找到了就跳到對(duì)應(yīng)方法執(zhí)行瘸洛。
  5. 如果上面2個(gè)地方都沒有找到IMP揍移,那么就啟動(dòng)消息轉(zhuǎn)發(fā)機(jī)制,這就意味著反肋,你的方法會(huì)被編譯器轉(zhuǎn)換成C函數(shù)那伐。比如你的方法是下面這個(gè):
-(int)doComputeWithNum:(int)aNum 

將會(huì)被轉(zhuǎn)換成這樣:

int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum) 

ObjC runtime通過調(diào)用函數(shù)指針來調(diào)用這些方法,你不能直接調(diào)用這些轉(zhuǎn)換后的方法石蔗,但是Cocoa框架提供了一個(gè)方法去獲得方法的指針

 //declare C function pointer
 int (computeNum *)(id,SEL,int);
 
 //methodForSelector is COCOA & not ObjC Runtime
 //gets the same function pointer objc_msgSend gets
 computeNum = (int (*)(id,SEL,int))[target    methodForSelector:@selector(doComputeWithNum:)];
 
 //execute the C function pointer returned by the runtime
 computeNum(obj,@selector(doComputeWithNum:),aNum); 

通過這種方式罕邀,你可以直接在runtime時(shí)調(diào)用某個(gè)方法。如果你想確保某個(gè)方法一定會(huì)執(zhí)行养距,你甚至可以用這種方式繞過runtime的動(dòng)態(tài)特性诉探。在ObjC中也是這么調(diào)用方法的,只不過用的是objc_msgSend()棍厌。

ObjC消息轉(zhuǎn)發(fā)

在ObjC中肾胯,允許發(fā)消息給一個(gè)不能響應(yīng)該消息的對(duì)象(有可能是蘋果故意這么設(shè)計(jì)的)。蘋果在他的文檔中給出的理由是為了模擬實(shí)現(xiàn)多重繼承機(jī)制(ObjC原生是不支持這種機(jī)制的)耘纱,或者你希望把你的設(shè)計(jì)抽象化敬肚,把具體的實(shí)現(xiàn)隱藏起來。消息轉(zhuǎn)發(fā)是runtime一個(gè)非常重要的能力揣炕。它的工作方式大概是這樣:

  1. runtime在你的類及其父類的的Class Cache和方法分發(fā)表中都沒有找到目標(biāo)方法。
  2. 然后runtime將會(huì)調(diào)用你的類的+ (BOOL) resolveInstanceMethod:(SEL)aSEL方法东跪。這就給了你一個(gè)機(jī)會(huì)去提供一個(gè)方法的實(shí)現(xiàn)并告訴runtime這個(gè)方法是可用的畸陡,如果runtime開始查找方法就可以找到你提供的這個(gè)方法。你可以這樣做虽填,定義一個(gè)方法:
void fooMethod(id obj, SEL _cmd)
{
 NSLog(@"Doing Foo");
}

class_addMethod()將其添加到類的方法列表中

+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(doFoo:)){
        class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

class_addMethod()最后一個(gè)參數(shù)"v@:"是前面指定方法的返回值和參數(shù)丁恭。你可以在這篇文檔中查看這個(gè)參數(shù)可以放哪些值。

  1. 如果第2步?jīng)]能解決問題斋日,那么runtime將會(huì)調(diào)用- (id)forwardingTargetForSelector:(SEL)aSelector方法牲览,這就再次給了我們機(jī)會(huì)去告訴runtime將消息轉(zhuǎn)發(fā)給另一個(gè)對(duì)象來處理。最好在這一步之前對(duì)消息做處理恶守,因?yàn)橄乱徊降拈_銷較大第献,你可以這么做:
 - (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

當(dāng)然你不能在這個(gè)方法中返回self,因?yàn)檫@樣會(huì)造成死循環(huán)兔港。

  1. 如果在上一步時(shí)沒做處理庸毫,那么runtime將會(huì)調(diào)用- (void)forwardInvocation:(NSInvocation *)anInvocation最后一次嘗試給目標(biāo)對(duì)象發(fā)送消息。NSInvocation本質(zhì)上是一條ObjC消息封裝成的對(duì)象衫樊,只要你拿到了一個(gè)NSInvocation飒赃,你就可以改變一條消息的任何值利花,比如目標(biāo)對(duì)象,方法選擇器或者參數(shù)等等载佳,你可以想這樣做:
-(void)forwardInvocation:(NSInvocation *)invocation
{
    SEL invSEL = invocation.selector;
 
    if([altObject respondsToSelector:invSEL]) {
        [invocation invokeWithTarget:altObject];
    } else {
        [self doesNotRecognizeSelector:invSEL];
    }
}

如果你的類是繼承自NSObject炒事,那么它的- (void)forwardInvocation:(NSInvocation *)anInvocation方法的默認(rèn)實(shí)現(xiàn)只是簡單的調(diào)用了-doesNotRecognizeSelector:方法,如果你想最后一次對(duì)這條消息做處理蔫慧,可以重寫這個(gè)方法挠乳。

健壯的實(shí)例變量(Modern Runtime)

我們最近在現(xiàn)代runtime中得到的是健壯實(shí)例變量(Non Fragile ivars)的概念。編譯時(shí)藕漱,我們定義的變量是以在類中的偏移地址訪問的欲侮,而且這些工作編譯器能自動(dòng)幫我們完成,這牽扯到底層的細(xì)節(jié)肋联,大致類似于:先得到一個(gè)指針指向創(chuàng)建的對(duì)象威蕉,然后基于該對(duì)象的起始地址,再根據(jù)變量的偏移地址我們就可以訪問到變量橄仍,最后根據(jù)變量的類型確定變量所占的內(nèi)存空間韧涨,所以編譯后變量的輸出形式(ivar layout)類似于下邊的表格,左邊一列數(shù)字代表偏移地址:



我們再NSObject類中有一些變量侮繁,然后我們創(chuàng)建一個(gè)類繼承它虑粥,并在這個(gè)類中添加一些新的成員變量。這樣做是沒問題的宪哩,直到蘋果發(fā)布了Mac OS X 10.x娩贷,就發(fā)生了下面的情況:



我們的子類中的成員變量被抹掉了,因?yàn)樵诟割愔械南嗤恢靡泊嬖诔蓡T變量锁孟。唯一的解決辦法就是蘋果恢復(fù)到以前的布局方式彬祖,但是如果這樣做的話,蘋果的框架就會(huì)變得很不先進(jìn)品抽,因?yàn)槌蓡T變量的位置布局是固定死的储笑。在不健壯的變量下,你不得不重新編譯你的類圆恤,使得其恢復(fù)兼容性突倍。那么健壯的成員變量下又是怎樣呢?

變量的的布局和非健壯變量情況下是一樣的盆昙,所以父類和子類中也會(huì)有重疊覆蓋的成員變量羽历,但是當(dāng)runtime檢測到有覆蓋的變量時(shí),它會(huì)調(diào)整子類中新增變量的位置淡喜,使得其不被覆蓋窄陡。

ObjC關(guān)聯(lián)對(duì)象

關(guān)聯(lián)引用(Associated References)是最近被引入 Mac OS X 10.6的一項(xiàng)特性。與某些其他語言不同拆火,ObjC不支持動(dòng)態(tài)的給一個(gè)類添加變量跳夭。之前在ObjC中你想"假裝"給一個(gè)類動(dòng)態(tài)的添加變量是需要花很多功夫的涂圆,關(guān)聯(lián)引用的引入就是為了解決這個(gè)問題,如果你想給任何一個(gè)已經(jīng)存在的類添加變量你可以這樣做币叹,比如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頭文件中查看怎么存儲(chǔ)通過objc_setAssociatedObject()方法設(shè)置的變量润歉。

/* 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
}; 

這和你用@property設(shè)置變量時(shí)的做法是一樣的。

Hybrid vTable Dispatch

如果你看過現(xiàn)代runtime的源碼你可能會(huì)遇到下面這一段話(在 objc-runtime-new.m中)

/***********************************************************************
* 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.
**********************************************************************/

大概的意思就是颈抚,runtime會(huì)試著把最常調(diào)用的那些方法放在這個(gè)vtable中踩衩,這樣會(huì)加速你app的運(yùn)行速度,因?yàn)檎{(diào)用在這個(gè)表中的方法用的指令比objc_msgSend少贩汉。這個(gè)vtable表由全局16個(gè)最常調(diào)用的方法組成驱富,再往下看,你能看到自動(dòng)垃圾回收和沒有自動(dòng)垃圾回收環(huán)境下的默認(rèn)方法:

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:", 
};

那你怎么知道你調(diào)用了這個(gè)vtable中的方法呢匹舞?調(diào)試的時(shí)候褐鸥,你能在方法調(diào)用棧中看到一些方法:

  • objc_msgSend_fixup 代表你調(diào)用了一個(gè)正準(zhǔn)備加入vtable的方法
  • objc_msgSend_fixedup 代表你調(diào)用的方法原先在vtable中,現(xiàn)在不在了
  • objc_msgSend_vtable[0-15] 代表你調(diào)用了vtable中的某一個(gè)方法赐稽,后面的數(shù)字就是方法在table中的序號(hào)
    runtime可以隨意增加或者刪除vtable中的方法叫榕,所以一次運(yùn)行過程中objc_msgSend_vtable10對(duì)應(yīng)著-length方法,下一次運(yùn)行時(shí)就不一定了姊舵。

總結(jié)

希望你能喜歡這篇文章晰绎,這是我在Des Moines Cocoaheads演講時(shí)的內(nèi)容。ObjC runtime是一項(xiàng)浩大的工程括丁,它給我么你的Cocoa/ObjC應(yīng)用提供了動(dòng)力荞下,并且使很多強(qiáng)大的特性變成了可能。如果你還沒有看過蘋果的官方文檔 Objective-C Runtime Programming Guide史飞,Objective-C Runtime Reference尖昏,我希望你能看一遍。謝謝祸憋!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末会宪,一起剝皮案震驚了整個(gè)濱河市肖卧,隨后出現(xiàn)的幾起案子蚯窥,更是在濱河造成了極大的恐慌,老刑警劉巖塞帐,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拦赠,死亡現(xiàn)場離奇詭異,居然都是意外死亡葵姥,警方通過查閱死者的電腦和手機(jī)荷鼠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來榔幸,“玉大人允乐,你說我怎么就攤上這事矮嫉。” “怎么了牍疏?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵蠢笋,是天一觀的道長。 經(jīng)常有香客問我鳞陨,道長昨寞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任厦滤,我火速辦了婚禮援岩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘掏导。我一直安慰自己享怀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布碘菜。 她就那樣靜靜地躺著凹蜈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪忍啸。 梳的紋絲不亂的頭發(fā)上仰坦,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音计雌,去河邊找鬼悄晃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛凿滤,可吹牛的內(nèi)容都是我干的妈橄。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼翁脆,長吁一口氣:“原來是場噩夢啊……” “哼眷蚓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起反番,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤沙热,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后罢缸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體篙贸,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年枫疆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了爵川。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡息楔,死狀恐怖寝贡,靈堂內(nèi)的尸體忽然破棺而出扒披,到底是詐尸還是另有隱情,我是刑警寧澤圃泡,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布谎碍,位于F島的核電站,受9級(jí)特大地震影響洞焙,放射性物質(zhì)發(fā)生泄漏蟆淀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一澡匪、第九天 我趴在偏房一處隱蔽的房頂上張望熔任。 院中可真熱鬧,春花似錦唁情、人聲如沸疑苔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惦费。三九已至,卻和暖如春抢韭,著一層夾襖步出監(jiān)牢的瞬間薪贫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工刻恭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瞧省,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓鳍贾,卻偏偏與公主長得像鞍匾,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子骑科,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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

  • objc_getAssociatedObject返回與給定鍵的特定對(duì)象關(guān)聯(lián)的值橡淑。ID objc_getAssoci...
    有一種再見叫青春閱讀 1,564評(píng)論 0 7
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,679評(píng)論 0 9
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 727評(píng)論 0 2
  • 文中的實(shí)驗(yàn)代碼我放在了這個(gè)項(xiàng)目中咆爽。 以下內(nèi)容是我通過整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 913評(píng)論 0 6
  • 本文詳細(xì)整理了 Cocoa 的 Runtime 系統(tǒng)的知識(shí)梁棠,它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 792評(píng)論 0 4