原文地址:
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í)行的:
-
[MyObject alloc]
方法首先被執(zhí)行。MyObject類并沒有實(shí)現(xiàn)+alloc
方法碟贾,所以我們在MyObject類中查找這個(gè)方法會(huì)找不到币喧。 - 然后順著MyObject類的父類指針,在其父類NSObject中找到了
+alloc
方法袱耽。+alloc
方法檢查到消息的接受者是MyObject
類杀餐,會(huì)根據(jù)類的大小分配一塊內(nèi)存區(qū)域,使其isa指針指向MyObject類朱巨,并把+alloc
方法放入NSObject的Class Cache中史翘。于是我們就得到了一個(gè)MyObject類的實(shí)例對(duì)象。 - 然后
-init
方法被執(zhí)行,MyObejct類實(shí)現(xiàn)了這個(gè)方法恶座,方法被執(zhí)行搀暑,然后被加入MyObject的Class Cache。 - 然后是
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 在Part1,Part2森篷,Part3中進(jìn)行了詳細(xì)的講解卢未。大概意思就是:
- 檢查是否有可以忽略的方法堂飞,比如在垃圾回收的環(huán)境下我們可以忽略
-retain
悔叽,-release
等方法。 - 檢查目標(biāo)對(duì)象是否為nil周蹭。不像其他語言趋艘,在ObjectC中給一個(gè)nil對(duì)象發(fā)消息是完全可以的,并有時(shí)你會(huì)因?yàn)槟承┰蛳M@么去做凶朗,假設(shè)我們有一個(gè)非nil的目標(biāo)對(duì)象瓷胧,然后我們繼續(xù)...
- 然后我們需要在目標(biāo)類中查找IMP,首先在類的Class Cache中查找棚愤,如果找到了就順著指針找到要執(zhí)行的方法搓萧。
- 如果在Class Cache中沒有找到IMP杂数,那么就到類的方法列表中查找,如果找到了就跳到對(duì)應(yīng)方法執(zhí)行瘸洛。
- 如果上面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è)非常重要的能力揣炕。它的工作方式大概是這樣:
- runtime在你的類及其父類的的Class Cache和方法分發(fā)表中都沒有找到目標(biāo)方法。
- 然后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ù)可以放哪些值。
- 如果第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)兔港。
- 如果在上一步時(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尖昏,我希望你能看一遍。謝謝祸憋!