理解 Objective-C Runtime

來源:互聯網
上載者:User

標籤:

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

這段代碼是這樣執行的:

  1. [MyObject alloc] 先被執行。但是由於 MyObject 這個類沒有 +alloc 這個方法,於是去父類 NSObject 尋找。
  2. 檢測 NSObject 是否響應 +alloc 方法,發現響應,於是檢測 MyObject 類,根據其所需的記憶體空間大小開始分配記憶體空間,然後把 isa 指標指向 MyObject 類。那麼 +alloc 就被加進 cache 列表裡面了。
  3. 完了執行 -init 方法,因為 MyObject 響應該方法,直接加入 cache。
  4. 執行 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 裡面找到了,就調用它對應的函數指標。如果找不到,那就會執行一些其他的東西。步驟如下:

  1. 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了記憶體回收就不理會 retain, release 這些函數了。
  2. 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因為會被忽略掉。
  3. 如果上面兩個都過了,那就開始尋找這個類的 IMP,先從 cache 裡面找,完了找得到就跳到對應的函數去執行。
  4. 如果 cache 找不到就找一下方法分發表。
  5. 如果還找不到就要開始訊息轉寄邏輯了。

在編譯的時候,你定義的方法比如:

-(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 非常重要的一個特性,大概的步驟如下:

  1. 尋找該類及其父類的 cahce 和方法分發表,在找不到的情況下執行2。
  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。

  3. 接下來 Runtime 會調用 – (id)forwardingTargetForSelector:(SEL)aSelector 方法。
    這就給了程式員第二次機會,如果你沒辦法在自己的類裡面找到替代方法,你就重載這個方法,然後把訊息轉給其他的Object。

    - (id)forwardingTargetForSelector:(SEL)aSelector{    if(aSelector == @selector(mysteriousMethod:)){        return alternateObject;    }    return [super forwardingTargetForSelector:aSelector];}

    這樣你就可以把訊息轉給別人了。當然這裡你不能 return self,不然就死迴圈了=.=

  4. 最後,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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.