標籤:
http://www.justinyan.me/post/1624
註:本文是對 Colin Wheeler 的 Understanding the Objective-C Runtime 的翻譯。
初學 Objective-C(以下簡稱ObjC) 的人很容易忽略一個 ObjC 特性 —— ObjC Runtime。這是因為這門語言很容易上手,幾個小時就能學會怎麼使用,所以程式員們往往會把時間都花在瞭解 Cocoa 架構以及調整自己的程式的表現上。然而 Runtime 應該是每一個 ObjC 都應該要瞭解的東西,至少要理解編譯器會把
[target doMethodWith:var1];
編譯成:
objc_msgSend(target,@selector(doMethodWith:),var1);
這樣的語句。理解 ObjC Runtime 的工作原理,有助於你更深入地去理解 ObjC 這門語言,理解你的 App 是怎樣跑起來的。我想所有的 Mac/iPhone 開發人員,無論水平如何,都會從中獲益的。
ObjC Runtime 是開源的
ObjC Runtime 的代碼是開源的,可以從這個網站下載: opensource.apple.com。
這個是所有開原始碼的連結: http://www.opensource.apple.com/source/
這個是ObjC rumtime 的原始碼: http://www.opensource.apple.com/source/objc4/
4應該代表的是build版本而不是語言版本,現在是ObjC 2.0
動態 vs 靜態語言
ObjC 是一種面向runtime(運行時)的語言,也就是說,它會儘可能地把代碼執行的決策從編譯和連結的時候,延遲到運行時。這給程式員寫代碼帶來很大的靈活性,比如說你可以把訊息轉寄給你想要的對象,或者隨意交換一個方法的實現之類的。這就要求 runtime 能檢測一個對象是否能對一個方法進行響應,然後再把這個方法分發到對應的對象去。我們拿 C 來跟 ObjC 對比一下。在 C 語言裡面,一切從 main 函數開始,程式員寫代碼的時候是自上而下地,一個 C 的結構體或者說類吧,是不能把方法調用轉寄給其他對象的。舉個栗子:
#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 %rbpLlabel1: movq %rsp, %rbpLlabel2: subq $16, %rspLlabel3: 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 retLeh_func_end1: .cstringLC: .asciz "Hello World!"
然後,再連結 include 的庫,完了產生可執行代碼。對比一下 ObjC,當我們初學這門語言的時候教程是這麼說滴:用中括弧括起來的語句,
[self doSomethingWithVar:var1];
被編譯器編譯之後會變成:
objc_msgSend(self,@selector(doSomethingWithVar:),var1);
一個 C 方法,傳入了三個變數,self指標,要執行的方法 @selector(doSomethingWithVar:) 還有一個參數 var1。但是在這之後就不曉得發生什麼了。
什麼是 Objective-C Runtime?
ObjC Runtime 其實是一個 Runtime 庫,基本上用 C 和彙編寫的,這個庫使得 C 語言有了物件導向的能力(腦中浮現當你喬幫主參觀了施樂帕克的 SmallTalk 之後嘴角一抹淺笑)。這個庫做的事前就是載入類的資訊,進行方法的分發和轉寄之類的。
Objective-C Runtime 術語
再往下深談之前咱先介紹幾個術語。
- 2 Runtimes
目前說來Runtime有兩種,一個 Modern Runtime 和一個 Legacy Runtime。Modern Runtime 覆蓋了64位的Mac OS X Apps,還有 iOS Apps,Legacy Runtime 是早期用來給32位 Mac OS X Apps 用的,也就是可以不用管就是了。
- 2 Basic types of Methods
一種 Instance Method,還有 Class Method。instance method 就是帶“-”號的,需要執行個體化才能用的,如 :
-(void)doFoo; [aObj doFoot];
Class Method 就是帶“+”號的,類似於靜態方法可以直接調用:
+(id)alloc;[ClassName alloc];
這些方法跟 C 函數一樣,就是一組代碼,完成一個比較小的任務。
-(NSString *)movieTitle{ return @"Futurama: Into the Wild Green Yonder";}
- Selector
一個 Selector 事實上是一個 C 的結構體,表示的是一個方法。定義是:
typedef struct objc_selector *SEL;
使用起來就是:
SEL aSel = @selector(movieTitle);
這樣可以直接取一個selector,如果是傳遞訊息(類似於C的方法調用)就是:
[target getMovieTitleForObject:obj];
在 ObjC 裡面,用‘[]’括起來的運算式就是一個訊息。包括了一個 target,就是要接收訊息的對象,一個要被調用的方法還有一些你要傳遞的參數。類似於 C 函數的調用,但是又有所不同。事實上上面這個語句你僅僅是傳遞了 ObjC 訊息,並不代表它就會一定被執行。target 這個對象會檢測是誰發起的這個請求,然後決策是要執行這個方法還是其他方法,或者轉寄給其他的對象。
- Class
Class 的定義是這樣的:
typedef struct objc_class *Class;typedef struct objc_object { Class isa;} *id;
我們可以看到這裡這裡有兩個結構體,一個類結構體一個對象結構體。所有的 objc_object 對象結構體都有一個 isa 指標,這個 isa 指向它所屬的類,在運行時就靠這個指標來檢測這個對象是否可以響應一個 selector。完了我們看到最後有一個 id 指標。這個指標其實就只是用來代表一個 ObjC 對象,有點類似於 C++ 的泛型。當你拿到一個 id 指標之後,就可以擷取這個對象的類,並且可以檢測其是否響應一個 selector。這就是對一個 delegate 常用的調用方式啦。這樣說還有點抽象,我們看看 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};
可以看到一個 block 是被設計成一個對象的,擁有一個 isa 指標,所以你可以對一個 block 使用 retain, release, copy 這些方法。
- IMP (Method Implementations)
接下來看看啥是IMP。
typedef id (*IMP)(id self,SEL _cmd,...);
一個 IMP 就是一個函數指標,這是由編譯器產生的,當你發起一個 ObjC 訊息之後,最終它會執行的那個代碼,就是由這個函數指標指定的。
- Objective-C Classes
OK,回過頭來看看一個 ObjC 的類。舉一個栗子:
@interface MyClass : NSObject {//varsNSInteger 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
可以看到運行時一個類還關聯了它的父類指標,類名,成員變數,方法,cache 還有附屬的 protocol。
那麼類定義了對象並且自己也是個對象?這是咋整滴?
上面我提到過一個 ObjC 類同時也是一個對象,為了處理類和對象的關係,runtime 庫建立了一種叫做 標籤類 元類(Meta Class)的東西。當你發出一個訊息的時候,比方說
[NSObject alloc];
你事實上是把這個訊息發給了一個類對象(Class Object),這個類對象必須是一個 Meta Class 的執行個體,而這個 Meta Class 同時也是一個根 MetaClass 的執行個體。當你繼承了 NSObject 成為其子類的時候,你的類指標就會指向 NSObject 為其父類。但是 Meta Class 不太一樣,所有的 Meta Class 都指向根 Meta Class 為其父類。一個 Meta Class 持有所有能響應的方法。所以當 [NSObject alloc] 這條訊息發出的時候,objc_msgSend() 這個方法會去 NSObject 它的 Meta Class 裡面去尋找是否有響應這個 selector 的方法,然後對 NSObject 這個類對象執行方法調用。
為啥我們要繼承 Apple Classes
初學 Cocoa 開發的時候,多數教程都要我們繼承一個類比方 NSObject,然後我們就開始 Coding 了。比方說:
MyObject *object = [[MyObject alloc] init];
這個語句用來初始化一個執行個體,類似於 C++ 的 new 關鍵字。這個語句首先會執行 MyObject 這個類的 +alloc 方法,Apple 的官方文檔是這樣說的:
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.
建立的執行個體中,isa 成員變數會變初始化成一個資料結構體,用來描述所指向的類。其他的成員變數的記憶體會被置為0.
所以繼承 Apple 的類我們不僅是獲得了很多很好用的屬性,而且也繼承了這種記憶體配置的方法。
那麼啥是 Class Cache(objc_cache *cache)
剛剛我們看到 runtime 裡面有一個指標叫 objc_cache *cache,這是用來緩衝方法調用的。現在我們知道一個執行個體對象被傳遞一個訊息的時候,它會根據 isa 指標去尋找能夠響應這個訊息的對象。但是實際上我們在用的時候,只有一部分方法是常用的,很多方法其實很少用或者根本用不到。比如一個object你可能從來都不用copy方法,那我要是每次調用的時候還去遍曆一遍所有的方法那就太笨了。於是 cache 就應運而生了,每次你調用過一個方法,之後,這個方法就會被存到這個 cache 列表裡面去,下次調用的時候 runtime 會優先去 cache 裡面尋找,提高了調用的效率。舉一個栗子:
MyObject *obj = [[MyObject alloc] init]; // MyObject 的父類是 NSObject@implementation MyObject-(id)init { if(self = [super init]){ [self setVarA:@”blah”]; } return self;}@end
這段代碼是這樣執行的:
- [MyObject alloc] 先被執行。但是由於 MyObject 這個類沒有 +alloc 這個方法,於是去父類 NSObject 尋找。
- 檢測 NSObject 是否響應 +alloc 方法,發現響應,於是檢測 MyObject 類,根據其所需的記憶體空間大小開始分配記憶體空間,然後把 isa 指標指向 MyObject 類。那麼 +alloc 就被加進 cache 列表裡面了。
- 完了執行 -init 方法,因為 MyObject 響應該方法,直接加入 cache。
- 執行 self = [super init] 語句。這裡直接通過 super 關鍵字調用父類的 init 方法,確保父類初始化成功,然後再執行自己的初始化邏輯。
OK,這就是一個很簡單的初始化過程,在 NSObject 類裡面,alloc 和 init 沒做什麼特別重大的事情,但是,ObjC 特性允許你的 alloc 和 init 返回的值不同,也就是說,你可以在你的 init 函數裡面做一些很複雜的初始化操作,但是返回出去一個簡單的對象,這就隱藏了類的複雜性。再舉個栗子:
#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;@endint 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;}
如果你是ObjC的初學者,那麼你很可能會認為這段代碼執的輸出會是:
NSMutableArrayNSMutableArray NSArrayNSArrayMyObjectMyObject
但事實上是這樣的:
obj1 class is __NSPlaceholderArrayobj2 class is NSCFArrayobj3 class is __NSPlaceholderArrayobj4 class is NSCFArrayobj5 class is MyObjectobj6 class is MyObject
這是因為 ObjC 是允許運行 +alloc 返回一個特定的類,而 init 方法又返回一個不同的類的。可以看到 NSMutableArray 是對普通數組的封裝,內部實現是複雜的,但是對外隱藏了複雜性。
OK說回 objc_msgSend 這個方法
這個方法做的事情不少,舉個栗子:
[self printMessageWithString:@"Hello World!"];
這句語句被編譯成這樣:
objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");
這個方法先去尋找 self 這個對象或者其父類是否響應 @selector(printMessageWithString:),如果從這個類的方法分發表或者 cache 裡面找到了,就調用它對應的函數指標。如果找不到,那就會執行一些其他的東西。步驟如下:
- 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了記憶體回收就不理會 retain, release 這些函數了。
- 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因為會被忽略掉。
- 如果上面兩個都過了,那就開始尋找這個類的 IMP,先從 cache 裡面找,完了找得到就跳到對應的函數去執行。
- 如果 cache 找不到就找一下方法分發表。
- 如果還找不到就要開始訊息轉寄邏輯了。
在編譯的時候,你定義的方法比如:
-(int)doComputeWithNum:(int)aNum
會編譯成:
int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum)
然後由 runtime 去調用指向你的這個方法的函數指標。那麼之前我們說你發起訊息其實不是對方法的直接調用,其實 Cocoa 還是提供了可以直接調用的方法的:
// 首先定義一個 C 語言的函數指標int (computeNum *)(id,SEL,int);// 使用 methodForSelector 方法擷取對應與該 selector 的杉樹指標,跟 objc_msgSend 方法拿到的是一樣的// **methodForSelector 這個方法是 Cocoa 提供的,不是 ObjC runtime 庫提供的**computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)];// 現在可以直接調用該函數了,跟調用 C 函數是一樣的computeNum(obj,@selector(doComputeWithNum:),aNum);
如果你需要的話,你可以通過這種方式你來確保這個方法一定會被調用。
訊息轉寄機制
在 ObjC 這門語言中,發送訊息給一個並不響應這個方法的對象,是合法的,應該也是故意這麼設計的。換句話說,我可以對任意一個對象傳遞任意一個訊息(看起來有點像對任意一個類調用任意一個方法,當然事實上不是),當然如果最後找不到能調用的方法就會 Crash 掉。
Apple 設計這種機制的原因之一就是——用來類比多重繼承(ObjC 原生是不支援多重繼承的)。或者你希望把你的複雜設計隱藏起來。這種轉寄機制是 Runtime 非常重要的一個特性,大概的步驟如下:
- 尋找該類及其父類的 cahce 和方法分發表,在找不到的情況下執行2。
- 執行 + (BOOL) resolveInstanceMethod:(SEL)aSEL 方法。
這就給了程式員一次機會,可以告訴 runtime 在找不到改方法的情況下執行什麼方法。舉個栗子,先定義一個函數:
void fooMethod(id obj, SEL _cmd){ NSLog(@"Doing Foo");}
完了重載 resolveInstanceMethod 方法:
+(BOOL)resolveInstanceMethod:(SEL)aSEL{ if(aSEL == @selector(doFoo:)){ class_addMethod([self class],aSEL,(IMP)fooMethod,"[email protected]:"); return YES; } return [super resolveInstanceMethod];}
其中 “[email protected]:” 表示傳回值和參數,這個符號涉及 Type Encoding,可以參考Apple的文檔 ObjC Runtime Guide。
接下來 Runtime 會調用 – (id)forwardingTargetForSelector:(SEL)aSelector 方法。
這就給了程式員第二次機會,如果你沒辦法在自己的類裡面找到替代方法,你就重載這個方法,然後把訊息轉給其他的Object。
- (id)forwardingTargetForSelector:(SEL)aSelector{ if(aSelector == @selector(mysteriousMethod:)){ return alternateObject; } return [super forwardingTargetForSelector:aSelector];}
這樣你就可以把訊息轉給別人了。當然這裡你不能 return self,不然就死迴圈了=.=
- 最後,Runtime 會調用 – (void)forwardInvocation:(NSInvocation *)anInvocation 這個方法。NSInvocation 其實就是一條訊息的封裝。如果你能拿到 NSInvocation,那你就能修改這條訊息的 target, selector 和 arguments。舉個栗子:
-(void)forwardInvocation:(NSInvocation *)invocation{ SEL invSEL = invocation.selector; if([altObject respondsToSelector:invSEL]) { [invocation invokeWithTarget:altObject]; } else { [self doesNotRecognizeSelector:invSEL]; }}
預設情況下 NSObject 對 forwardInvocation 的實現就是簡單地執行 -doesNotRecognizeSelector: 這個方法,所以如果你想真正的在最後關頭去轉寄訊息你可以重載這個方法(好折騰-.-)。
原文後面介紹了 Non Fragile ivars (Modern Runtime), Objective-C Associated Objects 和 Hybrid vTable Dispatch。鑒於一是底層的可以不用理會,一是早司空見慣的不用詳談,還有一個是很簡單的,就是一個建立在方法分發表裡面填入預設常用的 method,所以有興趣的讀者可以自行查閱原文,這裡就不詳談鳥。
理解 Objective-C Runtime