翻譯自 Understanding the Objective-C Runtimehtml
Objective-C 的運行時(runtime)是剛剛瞭解 Cocoa/Objective-C 的人很容易忽視的一個特性。由於新手們經常花費了大量時間在 Cocoa 框架上以及如何調整和使用 Cocoa 框架,雖然 Objective-C 只須要幾個小時就能夠學會。每一個人都須要瞭解運行時具體是怎麼工做的,不只僅是知道 [target doMethodWith: var1]
會被編譯器翻譯成 objc_msgSend(target, @selector(doMethodWith:), var1)
。瞭解運行時會使你對 Objective-C 語言和你的 app 是怎麼工做的有更加深入的理解。 我認爲 Mac/iPhone 開發者不管經驗水平,都將從中受益。objective-c
Objective-C 運行時是開源的,隨時能夠從 opensource.apple.com 查看。事實上研究 Objective-C 是我是我除蘋果文檔之外,最初弄明白運行時是如何工做的幾種方法之一。編程
Objective-C 是面向運行時的語言,這意味着它將具體的執行,從編譯的時候和連接的時候推遲到它真正執行這段代碼的時候。這給了你很大的靈活性,能夠將消息重定向到適當的對象,或者你甚至能夠有意地交換方法實現,等等。這就要求一個「運行時」來完成對象的內省,來看該對象可否響應,以及是否合適派發某些方法。和 C 語言對比,在 C 語言中,你的程序從一個 main()
方法開始,它就像你寫的代碼那樣,自上而下的遵循着你的邏輯執行函數。一個 C 結構體不能將請求轉發到其餘目標上。極可能你有這樣一個程序:api
#include < stdio.h >
int main(int argc, const char **argv[]) {
printf("Hello World!");
return 0;
}
複製代碼
編譯器解析、優化,而後將你優化過的代碼轉換成彙編:數組
.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!"
複製代碼
而後將彙編代碼與一個庫連接起來,最終生成一個可執行文件。這與 Objective-C 不一樣,雖然過程類似,可是 ObjC 編譯器生成的代碼依賴於「運行時」庫的存在。剛認識 ObjC 時別人告訴咱們(在過度簡化的層面)咱們的 ObjC 方括號代碼發生了這些變化……緩存
[target doMethodWith:var1];
複製代碼
會被編譯器翻譯成bash
objc_msgSend(target, @selector(doMethodWith:), var1);
複製代碼
但除此以外,咱們對運行時所作的事情還不太瞭解。數據結構
Objective-C 運行時是一個運行時庫,主要由 C 和彙編語言寫成,給 C 語言增長了面向對象的功能以建立 Objective-C。這就是說它負責加載類信息,作全部方法分發、方法轉發等事情。Objective-C 的運行時本質上搭建了全部的基礎結構,使得 Objectict-C 的面向對象編程成爲可能。閉包
在咱們更深刻以前,爲了達成共識,讓咱們先了解一些術語。架構
現代運行時(全部 64 位 Mac OS X App 和全部 iOS app)和古老的運行時(全部 32 位 Mac OS X App)。
實例方法(例如 -(void)doFoo
)和類方法(例如 +(id)alloc
)。
就像 C 的「函數」同樣,是一組代碼,執行一個小任務:
- (NSString *)movieTitle {
return @"Futurama: Into the Wild Green Yonder";
}
複製代碼
Objective-C 中的選擇器本質上是一個 C struct,它能夠用來識別你想要對象執行的 Objective-C 方法。在運行時中它是這樣定義的:
typedef struct objc_selector *SEL;
複製代碼
是這樣用的:
SEL aSel = @selector(movieTitle);
複製代碼
[target getMovieTitleForObject:obj];
複製代碼
一個 Objective-C 消息包含中括號裏面的所有內容:消息的發送目標、但願目標執行的方法以及任何你發送給目標的參數。Objective-C 消息和 C 的函數調用類似可是不一樣。事實上你給一個對象發送的消息不表明它會執行。對象會檢查誰是消息的發送者,而後根據不一樣發送者執行不一樣的方法,或者轉發給其餘目標對象。
當你查看運行時中的一個類你會看到這個:
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
複製代碼
能夠看到有幾個東西。咱們有一個 Objective-C 類(Class)的結構體和一個對象(Object)的結構體。objc_object 裏只有一個定義爲 isa 的類指針,這就是咱們說的「isa 指針」。Objective-C 運行時只須要這個 isa 指針就能夠檢查一個對象,瞭解這個類是什麼,而後看它是否可以響應你發送的消息對應的選擇器。最後咱們看到了這個 id 指針。默認狀況下 id 指針只能告訴咱們這是一個 Objective-C 對象。當你有一個 id 指針時,你能夠查詢它的類,看它可否對某個方法做出響應,等等。當你知道你所指向的對象是什麼時,就能夠更具體地操做。
自己被設計成和運行時兼容,因此它們能夠看做是對象,能夠響應消息,例如 -retain
,-release
,-copy
等等。你能夠在 LLVM/Clang 的文檔裏看到 Block 的定義:
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
};
複製代碼
IMP(方法實現指針):
typedef id (*IMP)(id self, SEL _cmd, ...);
IMP 是編譯器爲你生成的方法實現的函數指針,Objective-C 新人不須要直接接觸 IMP,但 Objective-C 的運行時經過它來調用你的方法,咱們很快會看到。
Objective-C 類的基本實現以下:
@interface MyClass : NSObject {
//vars
NSInteger counter;
}
//methods
-(void)doFoo;
@end
複製代碼
可是運行時跟蹤記錄的比這要多:
#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
複製代碼
咱們能夠看到一個類有父類、名字、實例變量、方法、緩存和它聲明要遵照的協議等的引用。運行時須要這些信息來響應你的類或實例的消息。
是的,我以前說過類自己也是對象,運行時經過建立元類(Meta classes)來解決這個問題。當你發送一個像 [NSObject alloc]
這樣的消息時,你其實是在向類對象(Class object)發送一個消息。這個類對象須要是 MetaClass 的一個實例,繼而它自己就是根元類(root meta class)的一個實例。當你把你的一個類繼承於 NSObject
的時候,實質上是把你的類的 「superclass」 引用指向 NSObject
。全部元類也指向根元類做爲它的父類。元類裏只有它們能響應的類方法的列表。因此當咱們將一個消息發送給一個類對象,好比 [NSObject alloc]
時,objc_msgSend()
實際上會經過元類查看它能響應什麼方法,若是找到了一個方法,就會在類對象上運行。
當咱們剛開始接觸 Cocoa 編程時,教程告訴咱們建立的對象要繼承 NSObject,說只要繼承蘋果提供的類就能有不少好處。咱們不知道的是,這實際上是爲了讓咱們本身建立的對象可使用運行時。當咱們建立咱們一個類的實例時咱們這樣作:
MyObject *object = [[MyObject alloc] init];
複製代碼
第一個執行的方法是 +alloc
。Apple 文檔中說 「一個新實例的 isa 實例變量被初始化爲一個用於描述該實例對應類的數據結構;其餘實例變量內存都被設置爲 0」。因此繼承蘋果提供的類,咱們不只繼承了一些很好用的屬性,更重要的是能很容易地在內存中建立和運行時期待的結構相匹配的對象(有一個指向咱們的類的 isa
指針)。
當 Objective-C 運行時經過 isa
指針檢查一個對象時,它能夠找到一個實現了許多方法的對象。然而,你可能只調用其中的一小部分,所以每次執行查找類的分派表(dispatch table),搜索全部的 selector 是沒有意義的。因此類實現了一個緩存,每當你搜索一個類分派表,並找到對應的選擇器,它就把它放入緩存中。所以當 objc_msgSend()
經過一個類來查找一個選擇器時,它首先會搜索類緩存。這是基於這樣一種理論:若是一次在類上調用一條消息,那麼之後可能會再次調用相同的消息。若是咱們把緩存考慮進去,這意味着若是咱們有一個 NSObject
的子類 MyObject
並運行下面的代碼:
MyObject *obj = [[MyObject alloc] init];
@implementation MyObject
-(id)init {
if(self = [super init]){
[self setVarA:@」blah」];
}
return self;
}
@end
複製代碼
那麼會發生這幾件事:
[MyObject alloc]
首先被執行。MyObject
類沒有實現alloc,因此咱們在類中找不到 +alloc
,因而根據父類指針找到 NSObject
。NSObject
,得出它能響應 +alloc
。+alloc
檢查接受者類,即 MyObject
,而且分配一塊咱們類的大小的內存,並初始化它的 isa
指針指向 MyObject
類。如今咱們有了一個實例,最後咱們把 +alloc
放到 NSObject
類對象的類緩存中。-init
或者指定初始化方法(designated initializer)來調用一個實例方法(instance message)。固然,咱們的類對這個 -init
消息能夠做出響應,因此 -(id)init
被放入緩存。self = [super init]
被調用。super
是一個神奇的關鍵字,指向對象的父類,因此咱們到 NSObject
中調用它的 init
方法。這樣作是爲了確保 OOP 繼承正確地工做,全部的父類都將正確地初始化變量,而後你(在子類中)也能夠正確地初始化變量,再而後,若是須要的話,重寫父類的方法。對於 NSObject
來講,沒有什麼重要的事情發生,但事實並不是老是如此。有時會發生重要的初始化。例如這個…#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;
}
複製代碼
若是你是 Cocoa 新手,當我問你會打印出什麼你極可能會說:
NSMutableArray
NSMutableArray
NSArray
NSArray
MyObject
MyObject
複製代碼
但事實上結果是這樣:
obj1 class is __NSPlaceholderArray
obj2 class is NSCFArray
obj3 class is __NSPlaceholderArray
obj4 class is NSCFArray
obj5 class is MyObject
obj6 class is MyObject
複製代碼
這是由於在 Objective-C 中,有很大可能 +alloc
返回的類和 -init
返回的類不一樣。
不少事情。看一下這段代碼:
[self printMessageWithString:@"Hello World!"];
複製代碼
這實際上被編譯器翻譯成:
objc_msgSend(self, @selector(printMessageWithString:), @"Hello World!");
複製代碼
咱們順着目標對象的 isa 指針查找,看該對象(或者它其中一個父類)是否能響應 @selector(printMessageWithString:)
選擇器。假設咱們在分派表(dispatch table)或者緩存中找到了該選擇器,咱們會跟蹤函數指針並執行它。因此 objc_msgSend()
永遠不會返回,它開始執行,而後跟蹤一個指向你的方法的指針,而後你的方法返回,這看起來就像 objc_msgSend()
返回了同樣。
Bill Bumgarner 在(Part 1, Part 2 & Part 3)裏描述了更多 objc_msgSend()
的細節。總結一下他的文章結合你看到的 Objective-C 運行時代碼:
-retain
,-release
等調用。nil
目標。和其餘語言不一樣,在 ObjC 裏向 nil
發送消息十分合理而且有些狀況下確實想要這麼作。假如不是 nil
則繼續……這意味着最終你的代碼會被編譯器轉譯成 C 函數。你寫的某個方法多是這樣:
-(int)doComputeWithNum:(int)aNum
複製代碼
它會被轉換成……
int aClass_doComputeWithNum(aClass *self, SEL _cmd, int aNum)
複製代碼
ObjC 運行時會經過調用這些方法的函數指針來真正執行方法。我曾說過你不能直接調用這些轉譯後的方法,但其實 Cocoa 框架提供了一個獲取函數指針的方法……
// 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);
複製代碼
用這種方式你能夠直接訪問函數而且直接在運行時中執行它,甚至繞過運行時的動態特性(爲了確保指定的方法被執行)。ObjC 運行時也用這種方法來調用你的函數,只是用了 objc_msgSend()
。
在 Objective-C 中,發送消息給可能不能響應該消息的對象是合法的(多是有意設計的)。蘋果文檔裏提到可能的緣由一個是模擬 Objective-C 並不原生支持的多重繼承,或者是你想把真正接受消息的類或者對象隱藏起來。這也是運行時頗有必要的一件事。具體是這樣的:
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
。這給你提供了一個方法實現的機會,告訴運行時你已經解決了這個方法,若是它應該開始進行搜索,它將會找到方法。具體你能夠這樣作,定義一個函數: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()
的最後一部分中的 v@:
是該方法返回的內容,也是它的參數。你能夠在運行時指南的 Type Encodings 章節中瞭解能夠放入哪些內容。
- (id)forwardingTargetForSelector:(SEL)aSelector
。它所作的是給你一個機會,讓運行時指向在另外一個能夠響應消息的對象。最好在開銷更大的 - (void)forwardInvocation:(NSInvocation *)anInvocatio
方法接管以前調用,例如:{
if(aSelector == @selector(mysteriousMethod:)) {
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
複製代碼
顯然,你不想從這個方法中返回 self
,由於這樣會致使無限循環。
- (void)forwardInvocation:(NSInvocation *)anInvocation
。NSInvocation
本質上是一個 Objective-C 消息的的對象形式。一旦你有了一個 NSInvocation
,你基本上能夠改變任何信息,包括它的目標,選擇器和參數。例如你能夠作:- (void)forwardInvocation:(NSInvocation *)invocation {
SEL invSEL = invocation.selector;
if([altObject respondsToSelector:invSEL]) {
[invocation invokeWithTarget:altObject];
} else {
[self doesNotRecognizeSelector:invSEL];
}
}
複製代碼
若是你的對象繼承了 NSObject
, 默認狀況下 - (void)forwardInvocation:(NSInvocation *)anInvocation
實現會調用 -doesNotRecognizeSelector:
方法。你能夠重寫這個方法若是你想最後再作點什麼。
現代運行時新增長了不脆弱的(Non Fragile) ivars 的概念。當編譯你的類的時候,編譯器生成了一個實例變量內存佈局(ivar layout),來告訴運行時去那裏訪問你的類的實例變量們。這是一個底層實現細節:ivars 是實例變量分別相對於你的對象地址的偏移量,讀取 ivars 的字節數就是讀取的變量的大小。你的 ivar 佈局可能看起來像這樣(第一列是字節偏移量):
這裏咱們畫出了一個 NSObject
的實例變量內存佈局。咱們有一個繼承了 NSObject
的類,增長了一些新的實例變量。這沒什麼問題,直到蘋果發佈了新的 Mac OS X 10.x 系統,NSObject
忽然增長兩個新的實例變量,因而:
你的自定義對象和 NSObject
對象重疊的部分被清除。若是 Apple 永遠不改變以前的佈局能夠避免這種狀況,但若是他們那樣作,那麼他們的框架就永遠不會進步。在「脆弱的 ivars」 下,你必須從新編譯你從 Apple 繼承的類,來恢復兼容性。那麼在不脆弱的狀況下會發生什麼呢?
在不脆弱的 ivars 下,編譯器生成與脆弱 ivars 相同的 ivars 佈局。然而,當運行時檢測到和父類有重疊時,它會調整偏移量,以增長對類的補充,保留了在子類中添加的內容。
Mac OS X 10.6 Snow Leopard 中引入了關聯引用。Objective-C 沒有原生支持動態地將變量添加到對象上。所以,你須要不遺餘力構建基礎架構,以僞裝正在向類中添加一個變量。在 Mac OS X 10.6 中,Objective-C 運行時提供了原生支持。若是咱們想給每一個已經存在的類添加一個變量,好比 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()
的值的選項:
/* 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
語法中傳遞的選項相匹配。
若是你看一下現代運行時代碼,你會看到這個(在 objc-runtime-new.m)。
/***********************************************************************
* vtable dispatch
*
* 每一個類都有一個 vtable 指針。vtable 是一個 IMP 數組,
* 全部的類的 vtable 中表示的選擇器數量都是相同的。(i.e.
* 沒有一個類有更大或更小的 vtable).
* 每一個 vtable 索引都有一個關聯的蹦牀,該蹦牀在接收者類的
* vtable 的該索引處分派給 IMP(檢查 NULL 後)。分派
* fixup 使用了蹦牀而不是 objc_msgSend.
* 脆弱性:vtable 的大小和選擇器列表在啓動時已經設定好了。
* 編譯器生成的代碼沒法依賴於任何特定的vtable配置,甚至
* 根本不使用 vtable 調度。
* 內存大小:若是一個類的 vtable 和它的父類相同(i.e. 該類
* 沒有重寫任何 vtable 選擇器), 那麼這個類直接指向它的父
* 類的 vtable。這意味着被選中包含在 vtable 中的選擇器應
* 該有如下特色:
* (1) 常常被調用,可是 (2) 不常常被重寫。
* 特別的是,-dealloc 是一個壞的選擇。
* 轉發: 若是一個類沒有實現 vtable 中的部分選擇器, 這個類的
* vtable 中的這些選擇器的 IMP 會被設置成 objc_msgSend。
* +initialize: 每一個類保持默認的 vtable(老是重定向到
* objc_msgSend)直到其 +initialize 初始化方法完成。不然,
* 一個類的第一個消息多是一個 vtable 調度,而 vtable
* 蹦牀不包括 +initialize 初始化檢查。
* 改變: Categories, addMethod, 和 setImplementation 若是影響
* 到了 vtable 的選擇器,類和全部的子類的 vtable 都將強制重建。
**********************************************************************/
複製代碼
這背後的思想是,運行時試圖在這個 vtable 裏面存儲最常被調用的選擇器,這能夠給 app 加速,由於這比 objc_msgSend
使用了更少的指令。這個 vtable 包含 16 個最常被調用的選擇器,佔據了絕大部分全局調用的選擇器。你能夠看到垃圾回收 app 和非垃圾回收 app 的默認選擇器都是什麼。
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 中的方法了呢?你會在調試的堆棧跟蹤中看到如下幾個方法。這些方法你能夠當作調試版的 objc_msgSend()
。
objc_msgSend_fixup
表明 runtime 調用一個方法並正要把它加入到 vtable 中。objc_msgSend_fixedup
表明你調用方法曾經在 vtable 中,如今已經不在裏面了。objc_msgSend_vtable[0-15]
表明上述 vtable 中的一個經常使用方法。runtime 能夠隨意分配或取消它想要的值。因此這一次 objc_msgSend_vtable10
對應於 -length
方法,下一次運行可能對應方法就變了。我但願你喜歡這些,這篇文章大致上組成了我在我給 Des Moines Cocoaheads 的 ObjC 演講中提到的內容。ObjC 運行時寫的很棒,它提供了許多咱們在 Cocoa / Objective-C 中習覺得常的特性。若是你還沒看過 Apple 的 ObjC 運行時文檔,但願你去看一看。謝謝!