[譯]理解 Objective-C 執行時

金西西發表於2018-02-27

翻譯自 Understanding the Objective-C Runtime

[譯]理解 Objective-C 執行時

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 是我是我除蘋果文件以外,最初弄明白執行時是如何工作的幾種方法之一。

動態語言 vs 靜態語言

Objective-C 是面向執行時的語言,這意味著它將具體的執行,從編譯的時候和連結的時候推遲到它真正執行這段程式碼的時候。這給了你很大的靈活性,可以將訊息重定向到適當的物件,或者你甚至可以有意地交換方法實現,等等。這就要求一個“執行時”來完成物件的內省,來看該物件能否響應,以及是否合適派發某些方法。和 C 語言對比,在 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 %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];
複製程式碼

會被編譯器翻譯成

objc_msgSend(target, @selector(doMethodWith:), var1);
複製程式碼

但除此之外,我們對執行時所做的事情還不太瞭解。

什麼是 Objective-C 執行時?

Objective-C 執行時是一個執行時庫,主要由 C 和組合語言寫成,給 C 語言增加了物件導向的功能以建立 Objective-C。這就是說它負責載入類資訊,做所有方法分發、方法轉發等事情。Objective-C 的執行時本質上搭建了所有的基礎結構,使得 Objectict-C 的物件導向程式設計成為可能。

Objective-C 執行時的術語

在我們更深入之前,為了達成共識,讓我們先了解一些術語。

  • 2 種執行時:

現代執行時(所有 64 位 Mac OS X App 和所有 iOS app)和古老的執行時(所有 32 位 Mac OS X App)。

  • 2 種方法:

例項方法(例如 -(void)doFoo)和類方法(例如 +(id)alloc)。

  • 方法:

就像 C 的“函式”一樣,是一組程式碼,執行一個小任務:

- (NSString *)movieTitle {
  return @"Futurama: Into the Wild Green Yonder";
}
複製程式碼
  • Selector(選擇器):

Objective-C 中的選擇器本質上是一個 C struct,它可以用來識別你想要物件執行的 Objective-C 方法。在執行時中它是這樣定義的:

typedef struct objc_selector *SEL;
複製程式碼

是這樣用的:

SEL aSel = @selector(movieTitle);
複製程式碼
  • 訊息:
[target getMovieTitleForObject:obj];
複製程式碼

一個 Objective-C 訊息包含中括號裡面的全部內容:訊息的傳送目標、希望目標執行的方法以及任何你傳送給目標的引數。Objective-C 訊息和 C 的函式呼叫相似但是不同。事實上你給一個物件傳送的訊息不代表它會執行。物件會檢查誰是訊息的傳送者,然後根據不同傳送者執行不同的方法,或者轉發給其他目標物件。

  • 類(class):

當你檢視執行時中的一個類你會看到這個:

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 指標時,你可以查詢它的類,看它能否對某個方法作出響應,等等。當你知道你所指向的物件是什麼時,就可以更具體地操作。

  • 閉包(Block):

本身被設計成和執行時相容,所以它們可以看作是物件,可以響應訊息,例如 -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 類:

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 
複製程式碼

我們可以看到一個類有父類、名字、例項變數、方法、快取和它宣告要遵守的協議等的引用。執行時需要這些資訊來響應你的類或例項的訊息。

所以類(Class)定義物件(Object),但類本身就是物件,這是怎麼做到的?

是的,我之前說過類本身也是物件,執行時通過建立元類(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 指標)。

那麼類快取(Class Cache)是什麼呢?(objc_cache *cache)

當 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
複製程式碼

那麼會發生這幾件事:

  1. [MyObject alloc] 首先被執行。MyObject 類沒有實現alloc,所以我們在類中找不到 +alloc,於是根據父類指標找到 NSObject
  2. 我們詢問 NSObject,得出它能響應 +alloc+alloc 檢查接受者類,即 MyObject,並且分配一塊我們類的大小的記憶體,並初始化它的 isa 指標指向 MyObject 類。現在我們有了一個例項,最後我們把 +alloc 放到 NSObject 類物件的類快取中。
  3. 剛才我們傳送的是一個類訊息(class message),但現在我們通 過呼叫 -init 或者指定初始化方法(designated initializer)來呼叫一個例項方法(instance message)。當然,我們的類對這個 -init 訊息可以作出響應,所以 -(id)init 被放入快取。
  4. 然後 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 返回的類不同。

所以到底 objc_msgSend 發生了什麼?

很多事情。看一下這段程式碼:

    [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 執行時程式碼:

  1. 檢查被忽略的選擇器和短路。顯然,如果我們在垃圾收集下執行,我們可以忽略 -retain-release 等呼叫。
  2. 檢查 nil 目標。和其他語言不同,在 ObjC 裡向 nil 傳送訊息十分合理並且有些情況下確實想要這麼做。假如不是 nil 則繼續……
  3. 接下來在類中找到 IMP,首先通過類快取來查詢,如果找到就跟隨指標跳轉到對應的函式
  4. 如果在快取中找不到 IMP,則通過分派表來查詢,如果找到就跟隨指標跳轉到對應的函式
  5. 如果這兩個地方都找不到 IMP,則跳轉到轉發(forwarding)機制。

這意味著最終你的程式碼會被編譯器轉譯成 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()

Objecetive-C 訊息轉發

在 Objective-C 中,傳送訊息給可能不能響應該訊息的物件是合法的(可能是有意設計的)。蘋果文件裡提到可能的原因一個是模擬 Objective-C 並不原生支援的多重繼承,或者是你想把真正接受訊息的類或者物件隱藏起來。這也是執行時很有必要的一件事。具體是這樣的:

  1. 執行時搜尋類快取、類分派表以及父類的所有方法,沒有找到指定的方法。
  2. 執行時對你的類呼叫 + (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 章節中瞭解可以放入哪些內容。

  1. 如果我們不能解析該方法,執行時會繼續呼叫 - (id)forwardingTargetForSelector:(SEL)aSelector。它所做的是給你一個機會,讓執行時指向在另一個可以響應訊息的物件。最好在開銷更大的 - (void)forwardInvocation:(NSInvocation *)anInvocatio 方法接管之前呼叫,例如:
    {
      if(aSelector == @selector(mysteriousMethod:)) {
          return alternateObject;
      }
      return [super forwardingTargetForSelector:aSelector];
    }
複製程式碼

顯然,你不想從這個方法中返回 self,因為這樣會導致無限迴圈。

  1. 執行時最後一次嘗試傳送一個訊息傳送到它的預定目標,呼叫 - (void)forwardInvocation:(NSInvocation *)anInvocationNSInvocation 本質上是一個 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) (現代執行時)

現代執行時新增加了不脆弱的(Non Fragile) ivars 的概念。當編譯你的類的時候,編譯器生成了一個例項變數記憶體佈局(ivar layout),來告訴執行時去那裡訪問你的類的例項變數們。這是一個底層實現細節:ivars 是例項變數分別相對於你的物件地址的偏移量,讀取 ivars 的位元組數就是讀取的變數的大小。你的 ivar 佈局可能看起來像這樣(第一列是位元組偏移量):

old ivars

這裡我們畫出了一個 NSObject 的例項變數記憶體佈局。我們有一個繼承了 NSObject 的類,增加了一些新的例項變數。這沒什麼問題,直到蘋果釋出了新的 Mac OS X 10.x 系統,NSObject 突然增加兩個新的例項變數,於是:

fragile ivars

你的自定義物件和 NSObject 物件重疊的部分被清除。如果 Apple 永遠不改變之前的佈局可以避免這種情況,但如果他們那樣做,那麼他們的框架就永遠不會進步。在“脆弱的 ivars” 下,你必須重新編譯你從 Apple 繼承的類,來恢復相容性。那麼在不脆弱的情況下會發生什麼呢?

non fragile ivars

在不脆弱的 ivars 下,編譯器生成與脆弱 ivars 相同的 ivars 佈局。然而,當執行時檢測到和父類有重疊時,它會調整偏移量,以增加對類的補充,保留了在子類中新增的內容。

Objective-C 關聯物件(Associated Objects)

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語法中傳遞的選項相匹配。

混合 vTable 分發

如果你看一下現代執行時程式碼,你會看到這個(在 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 執行時文件,希望你去看一看。謝謝!

相關文章